You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
notcurses/src/lib/fill.c

657 lines
20 KiB
C

#include "internal.h"
void ncplane_greyscale(ncplane *n){
for(unsigned y = 0 ; y < n->leny ; ++y){
for(unsigned x = 0 ; x < n->lenx ; ++x){
nccell* c = &n->fb[nfbcellidx(n, y, x)];
unsigned r, g, b;
nccell_fg_rgb8(c, &r, &g, &b);
int gy = rgb_greyscale(r, g, b);
nccell_set_fg_rgb8(c, gy, gy, gy);
nccell_bg_rgb8(c, &r, &g, &b);
gy = rgb_greyscale(r, g, b);
nccell_set_bg_rgb8(c, gy, gy, gy);
}
}
}
// if this is not polyfillable cell, we return 0. if it is, we attempt to fill
// it, then recurse out. return -1 on error, or number of cells filled on
// success. so a return of 0 means there's no work to be done here, and N means
// we did some work here, filling everything we could reach. out-of-plane is 0.
static int
ncplane_polyfill_inner(ncplane* n, unsigned y, unsigned x, const nccell* c, const char* filltarg){
struct topolyfill* stack = NULL;
if(create_polyfill_op(y, x, &stack) == NULL){
return -1;
}
int ret = 0;
struct topolyfill* s;
do{
s = stack;
stack = stack->next;
y = s->y;
x = s->x;
nccell* cur = &n->fb[nfbcellidx(n, y, x)];
const char* glust = nccell_extended_gcluster(n, cur);
//fprintf(stderr, "checking %d/%d (%s) for [%s]\n", y, x, glust, filltarg);
if(strcmp(glust, filltarg) == 0){
++ret;
if(nccell_duplicate(n, cur, c) < 0){
goto err;
}
//fprintf(stderr, "blooming from %d/%d ret: %d\n", y, x, ret);
if(y){
if(create_polyfill_op(y - 1, x, &stack) == NULL){
goto err;
}
}
if(y + 1 < n->leny){
if(create_polyfill_op(y + 1, x, &stack) == NULL){
goto err;
}
}
if(x){
if(create_polyfill_op(y, x - 1, &stack) == NULL){
goto err;
}
}
if(x + 1 < n->lenx){
if(create_polyfill_op(y, x + 1, &stack) == NULL){
goto err;
}
}
}
free(s);
}while(stack);
return ret;
err:
free(s);
while(stack){
struct topolyfill* tmp = stack->next;
free(stack);
stack = tmp;
}
return -1;
}
// at the initial step only, invalid ystart, xstart is an error, so explicitly check.
int ncplane_polyfill_yx(ncplane* n, int ystart, int xstart, const nccell* c){
if(ystart < 0){
if(ystart != -1){
logerror("invalid y: %d", ystart);
return -1;
}
ystart = n->y;
}
unsigned y = ystart;
if(xstart < 0){
if(xstart != -1){
logerror("invalid x: %d", xstart);
return -1;
}
xstart = n->x;
}
unsigned x = xstart;
if(y >= n->leny || x >= n->lenx){
logerror("invalid start: %u/%u (%u/%u)", y, x, n->leny, n->lenx);
return -1;
}
const nccell* cur = &n->fb[nfbcellidx(n, y, x)];
const char* targ = nccell_extended_gcluster(n, cur);
const char* fillegc = nccell_extended_gcluster(n, c);
//fprintf(stderr, "checking %d/%d (%s) for [%s]\n", y, x, targ, fillegc);
if(strcmp(fillegc, targ) == 0){
return 0;
}
int ret = -1;
// we need an external copy of this, since we'll be writing to it on
// the first call into ncplane_polyfill_inner()
char* targcopy = strdup(targ);
if(targcopy){
ret = ncplane_polyfill_inner(n, y, x, c, targcopy);
free(targcopy);
}
return ret;
}
static bool
check_gradient_channel_args(uint32_t ul, uint32_t ur, uint32_t bl, uint32_t br){
if(ncchannel_default_p(ul) || ncchannel_default_p(ur) ||
ncchannel_default_p(bl) || ncchannel_default_p(br)){
if(!(ncchannel_default_p(ul) && ncchannel_default_p(ur) &&
ncchannel_default_p(bl) && ncchannel_default_p(br))){
logerror("some (not all) channels were defaults");
return true;
}
}
if(ncchannel_alpha(ul) != ncchannel_alpha(ur) ||
ncchannel_alpha(ur) != ncchannel_alpha(bl) ||
ncchannel_alpha(bl) != ncchannel_alpha(br)){
logerror("channel alphas didn't match");
return true;
}
if(ncchannel_palindex_p(ul) || ncchannel_palindex_p(bl) ||
ncchannel_palindex_p(br) || ncchannel_palindex_p(ur)){
logerror("can't blend palette-indexed color");
return true;
}
return false;
}
// Given the four channels arguments, verify that:
//
// - if any is default foreground, all are default foreground
// - if any is default background, all are default background
// - all foregrounds must have the same alpha
// - all backgrounds must have the same alpha
// - palette-indexed color must not be used
bool check_gradient_args(uint64_t ul, uint64_t ur, uint64_t bl, uint64_t br){
if(check_gradient_channel_args(ncchannels_fchannel(ul), ncchannels_fchannel(ur),
ncchannels_fchannel(bl), ncchannels_fchannel(br))){
return true;
}
if(check_gradient_channel_args(ncchannels_bchannel(ul), ncchannels_bchannel(ur),
ncchannels_bchannel(bl), ncchannels_bchannel(br))){
return true;
}
return false;
}
// calculate both channels of a gradient at a particular point, knowing that
// we're using double halfblocks, into `c`->channels.
static inline void
calc_highgradient(nccell* c, uint32_t ul, uint32_t ur, uint32_t ll,
uint32_t lr, unsigned y, unsigned x,
unsigned ylen, unsigned xlen){
if(!ncchannel_default_p(ul)){
cell_set_fchannel(c, calc_gradient_channel(ul, ur, ll, lr,
y * 2, x, ylen, xlen));
cell_set_bchannel(c, calc_gradient_channel(ul, ur, ll, lr,
y * 2 + 1, x, ylen, xlen));
}else{
nccell_set_fg_default(c);
nccell_set_bg_default(c);
}
}
int ncplane_gradient2x1(ncplane* n, int y, int x, unsigned ylen, unsigned xlen,
uint32_t ul, uint32_t ur, uint32_t ll, uint32_t lr){
if(!notcurses_canutf8(ncplane_notcurses(n))){
logerror("highdef gradients require utf8");
return -1;
}
if(check_gradient_channel_args(ul, ur, ll, lr)){
return -1;
}
unsigned ystart, xstart;
if(check_geometry_args(n, y, x, &ylen, &xlen, &ystart, &xstart)){
return -1;
}
if(xlen == 1){
if(ul != ur || ll != lr){
logerror("horizontal channel variation in single column");
return -1;
}
}
int total = 0;
for(unsigned yy = ystart ; yy < ystart + ylen ; ++yy){
for(unsigned xx = xstart ; xx < xstart + xlen ; ++xx){
nccell* targc = ncplane_cell_ref_yx(n, yy, xx);
targc->channels = 0;
if(pool_blit_direct(&n->pool, targc, "", strlen(""), 1) <= 0){
return -1;
}
calc_highgradient(targc, ul, ur, ll, lr, yy - ystart, xx - xstart, ylen * 2, xlen);
++total;
}
}
return total;
}
// FIXME remove in abi3
int ncplane_highgradient_sized(ncplane* n, uint32_t ul, uint32_t ur,
uint32_t ll, uint32_t lr, int ylen, int xlen){
if(ylen < 1 || xlen < 1){
return -1;
}
if(!notcurses_canutf8(ncplane_notcurses_const(n))){
// this works because the uin32_ts we pass in will be promoted to uint64_ts
// via extension, and the space will employ the background. mwahh!
return ncplane_gradient(n, -1, -1, ylen, xlen, " ", 0, ul, ur, ll, lr);
}
return ncplane_gradient2x1(n, -1, -1, ylen, xlen, ul, ur, ll, lr);
}
int ncplane_gradient(ncplane* n, int y, int x, unsigned ylen, unsigned xlen,
const char* egc, uint16_t stylemask,
uint64_t ul, uint64_t ur, uint64_t bl, uint64_t br){
if(check_gradient_args(ul, ur, bl, br)){
return -1;
}
unsigned ystart, xstart;
if(check_geometry_args(n, y, x, &ylen, &xlen, &ystart, &xstart)){
return -1;
}
if(ylen == 1){
if(xlen == 1){
if(ul != ur || ur != br || br != bl){
logerror("channel variation in 1x1 area");
return -1;
}
}else{
if(ul != bl || ur != br){
logerror("vertical channel variation in single row");
return -1;
}
}
}else if(xlen == 1){
if(ul != ur || bl != br){
logerror("horizontal channel variation in single column");
return -1;
}
}
int total = 0;
for(unsigned yy = ystart ; yy < ystart + ylen ; ++yy){
for(unsigned xx = xstart ; xx < xstart + xlen ; ++xx){
nccell* targc = ncplane_cell_ref_yx(n, yy, xx);
targc->channels = 0;
if(nccell_load(n, targc, egc) < 0){
return -1;
}
targc->stylemask = stylemask;
calc_gradient_channels(&targc->channels, ul, ur, bl, br,
yy - ystart, xx - xstart, ylen, xlen);
++total;
}
}
return total;
}
int ncplane_stain(ncplane* n, int y, int x, unsigned ylen, unsigned xlen,
uint64_t tl, uint64_t tr, uint64_t bl, uint64_t br){
// Can't use default or palette-indexed colors in a gradient
if(check_gradient_args(tl, tr, bl, br)){
return -1;
}
unsigned ystart, xstart;
if(check_geometry_args(n, y, x, &ylen, &xlen, &ystart, &xstart)){
return -1;
}
int total = 0;
for(unsigned yy = ystart ; yy < ystart + ylen ; ++yy){
for(unsigned xx = xstart ; xx < xstart + xlen ; ++xx){
nccell* targc = ncplane_cell_ref_yx(n, yy, xx);
if(targc->gcluster){
calc_gradient_channels(&targc->channels, tl, tr, bl, br,
yy - ystart, xx - xstart, ylen, xlen);
}
++total;
}
}
return total;
}
int ncplane_format(ncplane* n, int y, int x, unsigned ylen,
unsigned xlen, uint16_t stylemask){
unsigned ystart, xstart;
if(check_geometry_args(n, y, x, &ylen, &xlen, &ystart, &xstart)){
return -1;
}
int total = 0;
for(unsigned yy = ystart ; yy < ystart + ylen ; ++yy){
for(unsigned xx = xstart ; xx < xstart + xlen ; ++xx){
nccell* targc = ncplane_cell_ref_yx(n, yy, xx);
targc->stylemask = stylemask;
++total;
}
}
return total;
}
// if we're a half block, reverse the channels. if we're a space, set both to
// the background. if we're a full block, set both to the foreground.
static int
rotate_channels(ncplane* src, const nccell* c, uint32_t* fchan, uint32_t* bchan){
const char* egc = nccell_extended_gcluster(src, c);
if(egc[0] == ' ' || egc[0] == 0){
*fchan = *bchan;
return 0;
}else if(strcmp(egc, "") == 0 || strcmp(egc, "") == 0){
uint32_t tmp = *fchan;
*fchan = *bchan;
*bchan = tmp;
return 0;
}else if(strcmp(egc, "") == 0){
*bchan = *fchan;
return 0;
}
logerror("invalid EGC for rotation [%s]", egc);
return -1;
}
static int
rotate_output(ncplane* dst, uint32_t tchan, uint32_t bchan){
dst->channels = ncchannels_combine(tchan, bchan);
if(tchan != bchan){
return ncplane_putegc(dst, "", NULL);
}
if(ncchannel_default_p(tchan) && ncchannel_default_p(bchan)){
return ncplane_putegc(dst, "", NULL);
}else if(ncchannel_default_p(tchan)){
return ncplane_putegc(dst, " ", NULL);
}
return ncplane_putegc(dst, "", NULL);
}
// rotation works at two levels:
// 1) each 1x2 block is rotated into a 1x2 block ala
// ab cw ca ccw ab ccw bd ccw dc ccw ca ccw ab
// cd --> db --> cd --> ac --> ba --> db --> cd
// 2) each 1x2 block is rotated into its new location
//
// Characters which can be rotated must be RGB, to differentiate full blocks,
// spaces, and nuls. For clockwise rotations:
//
// nul: converts to two half defaults
// space: converts to two half backgrounds
// full: converts to two half foregrounds
// upper: converts to half background + half foreground
// lower: converts to half foreground + half background
//
// Fore/background carry full channel, including transparency.
//
// Ideally, rotation through 360 degrees will restore the original 2x1 squre.
// Unfortunately, the case where a half block occupies a cell having the same
// fore- and background will see it rotated into a single full block. In
// addition, lower blocks eventually become upper blocks with their channels
// reversed. In general:
//
// if a "row" (the bottom or top halves) are the same forechannel, merge to a
// single full block of that color (what is its background?).
// if a "row" is two different channels, they become a upper block (why not
// lower?) having the two channels as fore- and background.
static int
rotate_2x1_cw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx){
nccell c1 = NCCELL_TRIVIAL_INITIALIZER;
nccell c2 = NCCELL_TRIVIAL_INITIALIZER;
if(ncplane_at_yx_cell(src, srcy, srcx, &c1) < 0){
return -1;
}
if(ncplane_at_yx_cell(src, srcy, srcx + 1, &c2) < 0){
nccell_release(src, &c1);
return -1;
}
// there can be at most 4 colors and 4 transparencies:
// - c1fg, c1bg, c2fg, c2bg, c1ftrans, c2ftrans, c1btrans, c2btrans
// but not all are necessarily used:
// - topleft gets lowerleft. if lowerleft is foreground, c1fg c1ftrans.
// otherwise, c1bg c1btrans
// - topright gets upperleft. if upperleft is foreground, c1fg c1ftrans.
// otherwise, c1bg c1btrans
// - botleft get botright. if botright is foreground, c2fg c2ftrans.
// otherwise, c2bg c2btrans
// - botright gets topright. if topright is foreground, c2fg c2ftrans.
// otherwise, c2bg c2btrans
uint32_t c1b = cell_bchannel_common(&c1);
uint32_t c2b = cell_bchannel_common(&c2);
uint32_t c1t = cell_fchannel_common(&c1);
uint32_t c2t = cell_fchannel_common(&c2);
int ret = 0;
ret |= rotate_channels(src, &c1, &c1t, &c1b);
ret |= rotate_channels(src, &c2, &c2t, &c2b);
// right char comes from two tops. left char comes from two bottoms. if
// they're the same channel, they become a:
//
// nul if the channel is default
// space if the fore is default
// full if the back is default
ncplane_cursor_move_yx(dst, dsty, dstx);
rotate_output(dst, c1b, c2b);
rotate_output(dst, c1t, c2t);
return ret;
}
static int
rotate_2x1_ccw(ncplane* src, ncplane* dst, int srcy, int srcx, int dsty, int dstx){
nccell c1 = NCCELL_TRIVIAL_INITIALIZER;
nccell c2 = NCCELL_TRIVIAL_INITIALIZER;
if(ncplane_at_yx_cell(src, srcy, srcx, &c1) < 0){
return -1;
}
if(ncplane_at_yx_cell(src, srcy, srcx + 1, &c2) < 0){
nccell_release(src, &c1);
return -1;
}
uint32_t c1b = cell_bchannel_common(&c1);
unsigned c2b = cell_bchannel_common(&c2);
unsigned c1t = cell_fchannel_common(&c1);
unsigned c2t = cell_fchannel_common(&c2);
int ret = 0;
ret |= rotate_channels(src, &c1, &c1t, &c1b);
ret |= rotate_channels(src, &c2, &c2t, &c2b);
ncplane_cursor_move_yx(dst, dsty, dstx);
rotate_output(dst, c1t, c2t);
rotate_output(dst, c1b, c2b);
return ret;
}
// copy 'newp' into 'n' after resizing 'n' to match 'newp'
static int
rotate_merge(ncplane* n, ncplane* newp){
unsigned dimy, dimx;
ncplane_dim_yx(newp, &dimy, &dimx);
int ret = ncplane_resize(n, 0, 0, 0, 0, 0, 0, dimy, dimx);
if(ret == 0){
for(unsigned y = 0 ; y < dimy ; ++y){
for(unsigned x = 0 ; x < dimx ; ++x){
const nccell* src = &newp->fb[fbcellidx(y, dimx, x)];
nccell* targ = &n->fb[fbcellidx(y, dimx, x)];
if(cell_duplicate_far(&n->pool, targ, newp, src) < 0){
return -1;
}
}
}
}
return ret;
}
// generate a temporary plane that can hold the contents of n, rotated 90°
static ncplane*
rotate_plane(ncplane* n){
int absy, absx;
ncplane_yx(n, &absy, &absx);
unsigned dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
if(dimx % 2 != 0){
return NULL;
}
const int newy = dimx / 2;
const int newx = dimy * 2;
struct ncplane_options nopts = {
.y = absy,
.x = absx,
.rows = newy,
.cols = newx,
.userptr = n->userptr,
.name = "copy",
};
ncplane* newp = ncplane_create(n, &nopts);
return newp;
}
int ncplane_rotate_cw(ncplane* n){
ncplane* newp = rotate_plane(n);
if(newp == NULL){
return -1;
}
unsigned dimy, dimx;
ncplane_dim_yx(n, &dimy, &dimx);
int centy, centx;
ncplane_center_abs(n, &centy, &centx);
// the topmost row consists of the leftmost two columns. the rightmost column
// of the topmost row consists of the top half of the top two leftmost cells.
// the penultimate column of the topmost row consists of the bottom half of
// the top two leftmost cells. work from the bottom up on the source, so we
// can copy to the top row from the left to the right.
int targx, targy = 0;
for(unsigned x = 0 ; x < dimx ; x += 2){
targx = 0;
for(int y = (int)dimy - 1 ; y >= 0 ; --y){
if(rotate_2x1_cw(n, newp, y, x, targy, targx)){
ncplane_destroy(newp);
return -1;
}
targx += 2;
}
++targy;
}
int ret = rotate_merge(n, newp);
ret |= ncplane_destroy(newp);
return ret;
}
int ncplane_rotate_ccw(ncplane* n){
ncplane* newp = rotate_plane(n);
if(newp == NULL){
return -1;
}
unsigned dimy, dimx, targdimy, targdimx;
ncplane_dim_yx(n, &dimy, &dimx);
ncplane_dim_yx(newp, &targdimy, &targdimx);
int x = (int)dimx - 2;
int y;
// Each row of the target plane is taken from a column of the source plane.
// As the target row grows (down), the source column shrinks (moves left).
for(unsigned targy = 0 ; targy < targdimy ; ++targy){
y = 0;
for(unsigned targx = 0 ; targx < targdimx ; targx += 2){
if(rotate_2x1_ccw(n, newp, y, x, targy, targx)){
ncplane_destroy(newp);
return -1;
}
++y;
}
x -= 2;
}
int ret = rotate_merge(n, newp);
ret |= ncplane_destroy(newp);
return ret;
}
#ifdef USE_QRCODEGEN
#include <qrcodegen/qrcodegen.h>
#define QR_BASE_SIZE 17
#define PER_QR_VERSION 4
static inline unsigned
qrcode_rows(unsigned version){
return QR_BASE_SIZE + (version * PER_QR_VERSION);
}
static inline unsigned
qrcode_cols(unsigned version){
return QR_BASE_SIZE + (version * PER_QR_VERSION);
}
int ncplane_qrcode(ncplane* n, unsigned* ymax, unsigned* xmax, const void* data, size_t len){
const ncblitter_e blitfxn = NCBLIT_2x1;
const int MAX_QR_VERSION = 40; // QR library only supports up to 40
if(*ymax <= 0 || *xmax <= 0){
return -1;
}
if(len == 0){
return -1;
}
const int starty = n->y;
const int startx = n->x;
if(*xmax > n->lenx - startx){
return -1;
}
if(*ymax > n->leny - starty){
return -1;
}
if(*ymax < qrcode_rows(1)){
return -1;
}
if(*xmax < qrcode_cols(1)){
return -1;
}
const int availsquare = *ymax * 2 < *xmax ? *ymax * 2 : *xmax;
int roomforver = (availsquare - QR_BASE_SIZE) / PER_QR_VERSION;
if(roomforver > MAX_QR_VERSION){
roomforver = MAX_QR_VERSION;
}
const size_t bsize = qrcodegen_BUFFER_LEN_FOR_VERSION(roomforver);
if(bsize < len){
return -1;
}
uint8_t* src = malloc(bsize);
uint8_t* dst = malloc(bsize);
if(src == NULL || dst == NULL){
free(src);
free(dst);
return -1;
}
unsigned r, g, b;
// FIXME default might not be all-white
if(ncplane_fg_default_p(n)){
r = g = b = 0xff;
}else{
ncplane_fg_rgb8(n, &r, &g, &b);
}
memcpy(src, data, len);
int ret = -1;
int yscale, xscale;
if(qrcodegen_encodeBinary(src, len, dst, qrcodegen_Ecc_HIGH, 1, roomforver, qrcodegen_Mask_AUTO, true)){
const int square = qrcodegen_getSize(dst);
uint32_t* rgba = malloc(square * square * sizeof(uint32_t));
if(rgba){
for(int y = starty ; y < starty + square ; ++y){
for(int x = startx ; x < startx + square ; ++x){
const bool pixel = qrcodegen_getModule(dst, x, y);
ncpixel_set_a(&rgba[y * square + x], 0xff);
ncpixel_set_rgb8(&rgba[y * square + x], r * pixel, g * pixel, b * pixel);
}
}
struct ncvisual* ncv = ncvisual_from_rgba(rgba, square, square * sizeof(uint32_t), square);
free(rgba);
if(ncv){
ret = square;
struct ncvisual_options vopts = {
.n = n,
.blitter = blitfxn,
};
if(ncvisual_blit(ncplane_notcurses(n), ncv, &vopts) == n){
ret = square;
}
ncvgeom geom;
ncvisual_geom(ncplane_notcurses(n), NULL, &vopts, &geom);
yscale = geom.scaley;
xscale = geom.scalex;
}
ncvisual_destroy(ncv);
}
}
free(src);
free(dst);
if(ret > 0){
ret = (ret - QR_BASE_SIZE) / PER_QR_VERSION;
*ymax = qrcode_rows(ret) / yscale;
*xmax = qrcode_cols(ret) / xscale;
return ret;
}
return -1;
}
#else
int ncplane_qrcode(ncplane* n, unsigned* ymax, unsigned* xmax, const void* data, size_t len){
(void)n;
(void)ymax;
(void)xmax;
(void)data;
(void)len;
return -1;
}
#endif