Repository: dankamongmen/notcurses (language: python)

Issue:
wrap notcurses with safe libsixel API
Together with @ctrlcctrlv, I maintain a fork of libsixel, attempting to deal with the mess that was left. It's rife with security problems, and its current API is unsalvageable.

I've suggested a safe API (otherwise equivalent to the existing one), but rather than try to overhaul the original libsixel code, I'd like to just expose some Notcurses code. We'd first need to bring our sixel quantizer up to libsixel's level in both quality and performance (see #1452). But once we do, this seems a good way forward for the ecosystem.
add sixel analysis PoC
i'd like a `sixelinspect` PoC that takes some filenames on the command line, and then analyzes sixel encoding for them. show the time taken to encode, and the resulting color difference details. this will be helpful for evaluating various replacements for our current sixel encoding/quantizing algorithm (see #1452).


Discussion / hints:
Is this related to the discussion had here: https://github.com/libsixel/libsixel/issues/50#issuecomment-962622276 ?
no, not at all =]


Fix patch:
diff --git a/CMakeLists.txt b/CMakeLists.txt
index e061918cdd..043c42f239 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -213,7 +213,7 @@ file(GLOB COMPATSRC CONFIGURE_DEPENDS src/compat/*.c)
 
 ############################################################################
 # libnotcurses-core (core shared library, core static library)
-file(GLOB NCCORESRCS CONFIGURE_DEPENDS src/lib/*.c src/lib/*.cpp)
+file(GLOB NCCORESRCS CONFIGURE_DEPENDS src/lib/*.c)
 add_library(notcurses-core SHARED ${NCCORESRCS} ${COMPATSRC})
 if(${USE_STATIC})
 add_library(notcurses-core-static STATIC ${NCCORESRCS} ${COMPATSRC})
diff --git a/NEWS.md b/NEWS.md
index b6c63645e0..e6861c9bf0 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -8,6 +8,7 @@ rearrangements of Notcurses.
   * Added `NCOPTION_CLI_MODE`, an alias for the bitwise OR of
     `NCOPTION_SCROLLING`, `NCOPTION_NO_CLEAR_BITMAPS`,
     `NCOPTION_NO_ALTERNATE_SCREEN`, and `NCOPTION_PRESERVE_CURSOR`.
+  * Added `ncvisual_from_sixel()`.
 
 * 3.0.1 (2021-12-14)
   * Added the `NCPLANE_OPTION_VSCROLL` flag. Creating an `ncplane` with this
diff --git a/USAGE.md b/USAGE.md
index 4461a553a1..93c29777f5 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -3382,6 +3382,9 @@ struct ncvisual* ncvisual_from_bgra(struct notcurses* nc, const void* bgra,
 struct ncvisual* ncvisual_from_palidx(const void* data, int rows,
                                       int rowstride, int cols, int palsize,
                                       int pstride, const uint32_t* palette);
+
+// Construct an ncvisual from a nul-terminated Sixel control sequence.
+struct ncvisual* ncvisual_from_sixel(const char* s, unsigned leny, unsigned lenx);
 ```
 
 `ncvisual`s can also be loaded from the contents of a plane:
@@ -3632,7 +3635,6 @@ have only one frame), until it returns `NCERR_EOF`:
 // Open a visual at 'file', extracting a codec and parameters.
 struct ncvisual* ncvisual_from_file(const char* file);
 
-
 // extract the next frame from an ncvisual. returns NCERR_EOF on end of file,
 // and NCERR_SUCCESS on success, otherwise some other NCERR.
 int ncvisual_decode(struct ncvisual* nc);
diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md
index 25130e93b2..5f88dcce5c 100644
--- a/doc/man/man3/notcurses_visual.3.md
+++ b/doc/man/man3/notcurses_visual.3.md
@@ -121,6 +121,8 @@ typedef struct ncvgeom {
 
 **int ncplane_qrcode(struct ncplane* ***n***, unsigned* ***ymax***, unsigned* ***xmax***, const void* ***data***, size_t ***len***)**
 
+**struct ncvisual* ncvisual_from_sixel(const char* ***s***, unsigned ***leny***, unsigned ***lenx***);**
+
 # DESCRIPTION
 
 An **ncvisual** is a virtual pixel framebuffer. They can be created from
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 5f91033888..f66c13c8cc 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -3153,6 +3153,10 @@ API ALLOC struct ncvisual* ncvisual_from_plane(const struct ncplane* n,
                                                unsigned leny, unsigned lenx)
   __attribute__ ((nonnull (1)));
 
+// Construct an ncvisual from a nul-terminated Sixel control sequence.
+API ALLOC struct ncvisual* ncvisual_from_sixel(const char* s, unsigned leny, unsigned lenx)
+  __attribute__ ((nonnull (1)));
+
 #define NCVISUAL_OPTION_NODEGRADE     0x0001ull // fail rather than degrade
 #define NCVISUAL_OPTION_BLEND         0x0002ull // use NCALPHA_BLEND with visual
 #define NCVISUAL_OPTION_HORALIGNED    0x0004ull // x is an alignment, not absolute
diff --git a/src/lib/blit.h b/src/lib/blit.h
new file mode 100644
index 0000000000..530506ef3c
--- /dev/null
+++ b/src/lib/blit.h
@@ -0,0 +1,23 @@
+#ifndef NOTCURSES_BLIT
+#define NOTCURSES_BLIT
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct ncplane;
+struct blitterargs;
+
+// scaledy and scaledx are output geometry from scaling; data is output data
+// from scaling. we might actually need more pixels due to framing concerns,
+// in which case just assume transparent input pixels where needed.
+typedef int (*ncblitter)(struct ncplane* n, int linesize, const void* data,
+                         int scaledy, int scaledx, const struct blitterargs* bargs);
+
+void set_pixel_blitter(ncblitter blitfxn);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/lib/blitset.h b/src/lib/blitset.h
index b82bb1c3e9..69ed55e3cf 100644
--- a/src/lib/blitset.h
+++ b/src/lib/blitset.h
@@ -5,6 +5,8 @@
 extern "C" {
 #endif
 
+#include "blit.h"
+
 // number of pixels that map to a single cell, height-wise
 static inline int
 encoding_y_scale(const tinfo* tcache, const struct blitset* bset) {
@@ -50,8 +52,6 @@ ncplot_defblitter(const notcurses* nc){
   return NCBLIT_1x1;
 }
 
-void set_pixel_blitter(ncblitter blitfxn);
-
 #ifdef __cplusplus
 }
 #endif
diff --git a/src/lib/internal.h b/src/lib/internal.h
index b48cfb2fa3..8539691e3c 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -390,12 +390,6 @@ typedef struct blitterargs {
   } u;
 } blitterargs;
 
-// scaledy and scaledx are output geometry from scaling; data is output data
-// from scaling. we might actually need more pixels due to framing concerns,
-// in which case just assume transparent input pixels where needed.
-typedef int (*ncblitter)(struct ncplane* n, int linesize, const void* data,
-                         int scaledy, int scaledx, const blitterargs* bargs);
-
 // a system for rendering RGBA pixels as text glyphs or sixel/kitty bitmaps
 struct blitset {
   ncblitter_e geom;
@@ -693,7 +687,6 @@ void sprixel_hide(sprixel* s);
 // dimy and dimx are cell geometry, not pixel.
 sprixel* sprixel_alloc(ncplane* n, int dimy, int dimx);
 sprixel* sprixel_recycle(ncplane* n);
-int sprite_init(const tinfo* t, int fd);
 int sprite_clear_all(const tinfo* t, fbuf* f);
 // these three all use absolute coordinates
 void sprixel_invalidate(sprixel* s, int y, int x);
@@ -1077,7 +1070,7 @@ ALLOC char* ncplane_vprintf_prep(const char* format, va_list ap);
 
 // Resize the provided ncvisual to the specified 'rows' x 'cols', but do not
 // change the internals of the ncvisual. Uses oframe.
-int ncvisual_blit_internal(struct ncvisual* ncv, int rows, int cols,
+int ncvisual_blit_internal(const struct ncvisual* ncv, int rows, int cols,
                            ncplane* n, const struct blitset* bset,
                            const blitterargs* bargs);
 
@@ -1794,8 +1787,8 @@ create_polyfill_op(int y, int x, struct topolyfill** stck){
 typedef struct ncvisual_implementation {
   int (*visual_init)(int loglevel);
   void (*visual_printbanner)(fbuf* f);
-  int (*visual_blit)(struct ncvisual* ncv, unsigned rows, unsigned cols, ncplane* n,
-                     const struct blitset* bset, const blitterargs* barg);
+  int (*visual_blit)(const struct ncvisual* ncv, unsigned rows, unsigned cols,
+                     ncplane* n, const struct blitset* bset, const blitterargs* barg);
   struct ncvisual* (*visual_create)(void);
   struct ncvisual* (*visual_from_file)(const char* fname);
   // ncv constructors other than ncvisual_from_file() need to set up the
diff --git a/src/lib/sixel.h b/src/lib/sixel.h
new file mode 100644
index 0000000000..a7fba923fa
--- /dev/null
+++ b/src/lib/sixel.h
@@ -0,0 +1,167 @@
+#ifndef NOTCURSES_SIXEL
+#define NOTCURSES_SIXEL
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#include <ctype.h>
+#include <stdlib.h>
+
+uint32_t* ncsixel_as_rgba(const char *sx, unsigned leny, unsigned lenx){
+#define MAXCOLORS 65535
+  // cast is necessary for c++ callers
+  uint32_t* rgba = (uint32_t*)malloc(sizeof(*rgba) * leny * lenx);
+  if(rgba == NULL){
+    return NULL;
+  }
+  // cast is necessary for c++ callers
+  uint32_t* colors = (uint32_t*)malloc(sizeof(*colors) * MAXCOLORS);
+  if(colors == NULL){
+    free(rgba);
+    return NULL;
+  }
+  // first we skip the header
+  while(*sx != '#'){
+    ++sx;
+  }
+  // now we build the color table (form: #Pc;Pu;Px;Py;Pz). data starts with
+  // a color spec lacking a semicolon.
+  enum {
+    STATE_WANT_HASH,
+    STATE_WANT_COLOR,
+    STATE_WANT_COLORSEMI,
+    STATE_WANT_COLORSPACE,
+    STATE_WANT_DATA,
+  } state = STATE_WANT_HASH;
+  unsigned color = 0;
+  unsigned x = 0;
+  unsigned y = 0;
+  unsigned rle = 1;
+  while(*sx){
+    if(*sx == '\e'){
+      break;
+    }
+    if(state == STATE_WANT_HASH){
+      if('#' != *sx){
+        goto err;
+      }
+      state = STATE_WANT_COLOR;
+    }else if(state == STATE_WANT_COLOR){
+      if(!isdigit(*sx)){
+        goto err;
+      }
+      color = 0;
+      do{
+        color *= 10;
+        color += *sx - '0';
+        ++sx;
+      }while(isdigit(*sx));
+//std::cerr << "Got color " << color << std::endl;
+      --sx;
+      state = STATE_WANT_COLORSEMI;
+    }else if(state == STATE_WANT_COLORSEMI){
+      // if we get a semicolon, we're a colorspec, otherwise data
+      if(*sx == ';'){
+        state = STATE_WANT_COLORSPACE;
+      }else{
+        state = STATE_WANT_DATA;
+        rle = 1;
+      }
+    }else if(state == STATE_WANT_COLORSPACE){
+      if('2' != *(sx++)){
+        goto err;
+      }
+      if(';' != *(sx++)){
+        goto err;
+      }
+      int r = 0;
+      do{
+        r *= 10;
+        r += *sx - '0';
+        ++sx;
+      }while(isdigit(*sx));
+      if(';' != *(sx++)){
+        goto err;
+      }
+      int g = 0;
+      do{
+        g *= 10;
+        g += *sx - '0';
+        ++sx;
+      }while(isdigit(*sx));
+      if(';' != *(sx++)){
+        goto err;
+      }
+      int b = 0;
+      do{
+        b *= 10;
+        b += *sx - '0';
+        ++sx;
+      }while(isdigit(*sx));
+      uint32_t rgb = htole(0xff000000 + (r << 16u) * 255 / 100 + (g << 8u) * 255 / 100 + b * 255 / 100);
+//std::cerr << "Got color " << color << ": " << r << "/" << g << "/" << b << std::endl;
+      if(color >= MAXCOLORS){
+        goto err;
+      }
+      colors[color] = rgb;
+      state = STATE_WANT_HASH;
+      --sx;
+    }
+    // read until we hit next colorspec
+    if(state == STATE_WANT_DATA){
+//std::cerr << "Character " << *sx << std::endl;
+      if(*sx == '#'){
+        state = STATE_WANT_HASH;
+        --sx;
+      }else if(*sx == '!'){ // RLE
+        ++sx;
+        rle = 0;
+        do{
+          rle *= 10;
+          rle += *sx - '0';
+          ++sx;
+        }while(isdigit(*sx));
+        if(2 >= rle){
+          goto err;
+        }
+        --sx;
+      }else if(*sx == '$'){
+        x = 0;
+        state = STATE_WANT_HASH;
+      }else if(*sx == '-'){
+        x = 0;
+        y += 6;
+        state = STATE_WANT_HASH;
+      }else{
+//std::cerr << "RLE: " << rle << " pos: " << y << "*" << x << std::endl;
+        for(unsigned xpos = x ; xpos < x + rle ; ++xpos){
+          for(unsigned ypos = y ; ypos < y + 6 ; ++ypos){
+            if((*sx - 63) & (1u << (ypos - y))){
+              // ought be an empty pixel
+//std::cerr << *s << " BMAP[" << ypos << "][" << xpos << "] = " << std::hex << colors[color] << std::dec << std::endl;
+              rgba[ypos * lenx + xpos] = colors[color];
+            }
+          }
+        }
+        x += rle;
+        rle = 1;
+      }
+    }
+    ++sx;
+  }
+  free(colors);
+  return rgba;
+
+err:
+  free(rgba);
+  free(colors);
+  return NULL;
+#undef MAXCOLORS
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/lib/sprite.h b/src/lib/sprite.h
index 48ee8a2cbf..b729343886 100644
--- a/src/lib/sprite.h
+++ b/src/lib/sprite.h
@@ -167,6 +167,7 @@ create_tam(int rows, int cols){
   return tam;
 }
 
+int sprite_init(const struct tinfo* t, int fd);
 int sixel_wipe(sprixel* s, int ycell, int xcell);
 // nulls out a cell from a kitty bitmap via changing the alpha value
 // throughout to 0. the same trick doesn't work on sixel, but there we
@@ -183,9 +184,9 @@ int kitty_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
 int fbcon_rebuild(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
 int kitty_rebuild_animation(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
 int kitty_rebuild_selfref(sprixel* s, int ycell, int xcell, uint8_t* auxvec);
-int sixel_draw(const tinfo* ti, const struct ncpile *p, sprixel* s,
+int sixel_draw(const struct tinfo* ti, const struct ncpile *p, sprixel* s,
                fbuf* f, int yoff, int xoff);
-int kitty_draw(const tinfo* ti, const struct ncpile *p, sprixel* s,
+int kitty_draw(const struct tinfo* ti, const struct ncpile *p, sprixel* s,
                fbuf* f, int yoff, int xoff);
 int kitty_move(sprixel* s, fbuf* f, unsigned noscroll, int yoff, int xoff);
 int sixel_scrub(const struct ncpile* p, sprixel* s);
@@ -208,8 +209,8 @@ int kitty_blit_selfref(struct ncplane* nc, int linesize, const void* data,
                        int leny, int lenx, const struct blitterargs* bargs);
 int fbcon_blit(struct ncplane* nc, int linesize, const void* data,
                int leny, int lenx, const struct blitterargs* bargs);
-int fbcon_draw(const tinfo* ti, sprixel* s, int yoff, int xoff);
-void fbcon_scroll(const struct ncpile* p, tinfo* ti, int rows);
+int fbcon_draw(const struct tinfo* ti, sprixel* s, int yoff, int xoff);
+void fbcon_scroll(const struct ncpile* p, struct tinfo* ti, int rows);
 void sixel_refresh(const struct ncpile* p, sprixel* s);
 
 // takes ownership of s on success.
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index 64a19f5c2a..a2a0fb93c5 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -13,6 +13,8 @@ extern "C" {
 #include <pthread.h>
 #include <stdbool.h>
 #include <notcurses/notcurses.h>
+#include "sprite.h"
+#include "blit.h"
 #include "fbuf.h"
 #include "in.h"
 
diff --git a/src/lib/visual.c b/src/lib/visual.c
index 93e77ff938..87858e4e27 100644
--- a/src/lib/visual.c
+++ b/src/lib/visual.c
@@ -3,6 +3,7 @@
 #include "builddef.h"
 #include "visual-details.h"
 #include "internal.h"
+#include "sixel.h"
 
 // ncvisual core code has a basic implementation in libnotcurses-core, and can
 // be augmented with a "multimedia engine" -- currently FFmpeg or OpenImageIO,
@@ -80,7 +81,7 @@ ncplane* ncvisual_subtitle_plane(ncplane* parent, const ncvisual* ncv){
   return visual_implementation->visual_subtitle(parent, ncv);
 }
 
-int ncvisual_blit_internal(ncvisual* ncv, int rows, int cols, ncplane* n,
+int ncvisual_blit_internal(const ncvisual* ncv, int rows, int cols, ncplane* n,
                            const struct blitset* bset, const blitterargs* barg){
   if(!(barg->flags & NCVISUAL_OPTION_NOINTERPOLATE)){
     if(visual_implementation->visual_blit){
@@ -796,6 +797,17 @@ ncvisual* ncvisual_from_rgba(const void* rgba, int rows, int rowstride, int cols
   return ncv;
 }
 
+ncvisual* ncvisual_from_sixel(const char* s, unsigned leny, unsigned lenx){
+  uint32_t* rgba = ncsixel_as_rgba(s, leny, lenx);
+  if(rgba == NULL){
+    logerror("failed converting sixel to rgba");
+    return NULL;
+  }
+  ncvisual* ncv = ncvisual_from_rgba(rgba, leny, lenx * sizeof(*rgba), lenx);
+  free(rgba);
+  return ncv;
+}
+
 ncvisual* ncvisual_from_rgb_packed(const void* rgba, int rows, int rowstride,
                                    int cols, int alpha){
   ncvisual* ncv = ncvisual_create();
diff --git a/src/media/ffmpeg.c b/src/media/ffmpeg.c
index db5fee5cfb..cc9ffb03f5 100644
--- a/src/media/ffmpeg.c
+++ b/src/media/ffmpeg.c
@@ -641,7 +641,7 @@ ffmpeg_resize(ncvisual* n, unsigned rows, unsigned cols){
 
 // rows/cols: scaled output geometry (pixels)
 static int
-ffmpeg_blit(ncvisual* ncv, unsigned rows, unsigned cols, ncplane* n,
+ffmpeg_blit(const ncvisual* ncv, unsigned rows, unsigned cols, ncplane* n,
             const struct blitset* bset, const blitterargs* bargs){
   void* data;
   int stride = 0;
diff --git a/src/media/oiio.cpp b/src/media/oiio.cpp
index 80879def25..e2a68b8811 100644
--- a/src/media/oiio.cpp
+++ b/src/media/oiio.cpp
@@ -144,7 +144,7 @@ int oiio_resize(ncvisual* nc, unsigned rows, unsigned cols) {
   return 0;
 }
 
-int oiio_blit(struct ncvisual* ncv, unsigned rows, unsigned cols,
+int oiio_blit(const ncvisual* ncv, unsigned rows, unsigned cols,
               ncplane* n, const struct blitset* bset,
               const blitterargs* bargs) {
 //fprintf(stderr, "%d/%d -> %d/%d on the resize\n", ncv->pixy, ncv->pixx, rows, cols);
diff --git a/src/media/oiio.h b/src/media/oiio.h
index 9b87d10f57..1f4ef2f42e 100644
--- a/src/media/oiio.h
+++ b/src/media/oiio.h
@@ -11,7 +11,7 @@ int oiio_decode(ncvisual* nc);
 struct ncvisual_details* oiio_details_init(void);
 void oiio_printbanner(fbuf* f);
 void oiio_details_seed(struct ncvisual* ncv);
-int oiio_blit(ncvisual* ncv, unsigned rows, unsigned cols,
+int oiio_blit(const ncvisual* ncv, unsigned rows, unsigned cols,
               struct ncplane* n, const struct blitset* bset,
               const blitterargs* bargs);
 ncvisual* oiio_from_file(const char* filename);


Test patch:
diff --git a/src/tests/sixel.cpp b/src/tests/sixel.cpp
index e267779cda..e89b28d357 100644
--- a/src/tests/sixel.cpp
+++ b/src/tests/sixel.cpp
@@ -2,131 +2,7 @@
 #include "lib/visual-details.h"
 #include <vector>
 #include <iostream>
-
-// convert the sprixel at s having pixel dimensions dimyXdimx to an rgb(a)
-// matrix for easier analysis. breaks on malformed sixels.
-std::vector<uint32_t> sixel_to_rgb(const char* s, size_t len, int dimy, int dimx) {
-  std::vector<uint32_t> bmap(dimy * dimx, 0x00000000ull);
-  std::vector<uint32_t> colors;
-  // first we skip the header
-  while(*s != '#'){
-    ++s;
-  }
-  // now we build the color table (form: #Pc;Pu;Px;Py;Pz). data starts with
-  // a color spec lacking a semicolon.
-  enum {
-    STATE_WANT_HASH,
-    STATE_WANT_COLOR,
-    STATE_WANT_COLORSEMI,
-    STATE_WANT_COLORSPACE,
-    STATE_WANT_DATA,
-  } state = STATE_WANT_HASH;
-  unsigned color = 0;
-  unsigned x = 0;
-  unsigned y = 0;
-  unsigned rle = 1;
-  const char* begin = s;
-  while((size_t)(s - begin) < len){
-    if(*s == '\e'){
-      break;
-    }
-    if(state == STATE_WANT_HASH){
-      REQUIRE('#' == *s);
-      state = STATE_WANT_COLOR;
-    }else if(state == STATE_WANT_COLOR){
-      CHECK(isdigit(*s));
-      color = 0;
-      do{
-        color *= 10;
-        color += *s - '0';
-        ++s;
-      }while(isdigit(*s));
-//std::cerr << "Got color " << color << std::endl;
-      --s;
-      state = STATE_WANT_COLORSEMI;
-    }else if(state == STATE_WANT_COLORSEMI){
-      // if we get a semicolon, we're a colorspec, otherwise data
-      if(*s == ';'){
-        state = STATE_WANT_COLORSPACE;
-      }else{
-        state = STATE_WANT_DATA;
-        rle = 1;
-      }
-    }else if(state == STATE_WANT_COLORSPACE){
-      CHECK('2' == *(s++));
-      CHECK(';' == *(s++));
-      int r = 0;
-      do{
-        r *= 10;
-        r += *s - '0';
-        ++s;
-      }while(isdigit(*s));
-      CHECK(';' == *(s++));
-      int g = 0;
-      do{
-        g *= 10;
-        g += *s - '0';
-        ++s;
-      }while(isdigit(*s));
-      CHECK(';' == *(s++));
-      int b = 0;
-      do{
-        b *= 10;
-        b += *s - '0';
-        ++s;
-      }while(isdigit(*s));
-      uint32_t rgb = htole(0xff000000 + (r << 16u) * 255 / 100 + (g << 8u) * 255 / 100 + b * 255 / 100);
-//std::cerr << "Got color " << color << ": " << r << "/" << g << "/" << b << std::endl;
-      if(color >= colors.capacity()){
-        colors.resize(color + 1);
-      }
-      colors[color] = rgb;
-      state = STATE_WANT_HASH;
-      --s;
-    }
-    // read until we hit next colorspec
-    if(state == STATE_WANT_DATA){
-//std::cerr << "Character " << *s << std::endl;
-      if(*s == '#'){
-        state = STATE_WANT_HASH;
-        --s;
-      }else if(*s == '!'){ // RLE
-        ++s;
-        rle = 0;
-        do{
-          rle *= 10;
-          rle += *s - '0';
-          ++s;
-        }while(isdigit(*s));
-        CHECK(2 < rle);
-        --s;
-      }else if(*s == '$'){
-        x = 0;
-        state = STATE_WANT_HASH;
-      }else if(*s == '-'){
-        x = 0;
-        y += 6;
-        state = STATE_WANT_HASH;
-      }else{
-//std::cerr << "RLE: " << rle << " pos: " << y << "*" << x << std::endl;
-        for(unsigned xpos = x ; xpos < x + rle ; ++xpos){
-          for(unsigned ypos = y ; ypos < y + 6 ; ++ypos){
-            if((*s - 63) & (1u << (ypos - y))){
-              // ought be an empty pixel
-              CHECK(0x00000000ull == bmap[ypos * dimx + xpos]);
-//std::cerr << *s << " BMAP[" << ypos << "][" << xpos << "] = " << std::hex << colors[color] << std::dec << std::endl;
-              bmap[ypos * dimx + xpos] = colors[color];
-            }
-          }
-        }
-        x += rle;
-        rle = 1;
-      }
-    }
-    ++s;
-  }
-  return bmap;
-}
+#include "lib/sixel.h"
 
 /*
 void print_bmap(const std::vector<uint32_t> rgba, int pixy, int pixx){
@@ -169,8 +45,8 @@ TEST_CASE("Sixels") {
     auto newn = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != newn);
     CHECK(0 == notcurses_render(nc_));
-    auto rgb = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
-                            newn->sprite->pixy, newn->sprite->pixx);
+    auto rgb = ncsixel_as_rgba(newn->sprite->glyph.buf, newn->sprite->pixy, newn->sprite->pixx);
+    REQUIRE(rgb);
     for(int y = 0 ; y < newn->sprite->pixy ; ++y){
       for(int x = 0 ; x < newn->sprite->pixx ; ++x){
 //fprintf(stderr, "%03d/%03d NCV: %08x RGB: %08x\n", y, x, ncv->data[y * newn->sprite->pixx + x], rgb[y * newn->sprite->pixx + x]);
@@ -191,8 +67,8 @@ TEST_CASE("Sixels") {
     vopts.flags = NCVISUAL_OPTION_NODEGRADE | NCVISUAL_OPTION_CHILDPLANE;
     auto newn = ncvisual_blit(nc_, ncv, &vopts);
     REQUIRE(nullptr != newn);
-    auto rgbold = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
-                               newn->sprite->pixy, newn->sprite->pixx);
+    auto rgbold = ncsixel_as_rgba(newn->sprite->glyph.buf, newn->sprite->pixy, newn->sprite->pixx);
+    REQUIRE(rgbold);
 //print_bmap(rgbold, newn->sprite->pixy, newn->sprite->pixx);
     CHECK(0 == notcurses_render(nc_));
     struct ncplane_options nopts = {
@@ -214,8 +90,8 @@ TEST_CASE("Sixels") {
     CHECK(0 == notcurses_render(nc_));
     // FIXME at this point currently, we get a degraded back of the orca
     // test via conversion back to image? unsure
-    auto rgbnew = sixel_to_rgb(newn->sprite->glyph.buf, newn->sprite->glyph.used,
-                               newn->sprite->pixy, newn->sprite->pixx);
+    auto rgbnew = ncsixel_as_rgba(newn->sprite->glyph.buf, newn->sprite->pixy, newn->sprite->pixx);
+    REQUIRE(rgbnew);
 //print_bmap(rgbnew, newn->sprite->pixy, newn->sprite->pixx);
     CHECK(0 == ncplane_destroy(newn));
     CHECK(0 == ncplane_destroy(blockerplane));