Code:isochromic.c

From the change wiki
// Elie's isochromic image generator v0.6
// Copyright 2022, Elie Goldman Smith
// License: GNU GPL V3
// To compile this code:
//    gcc isochromic.c -lm -lfftw3 -lreadline -lcurses -O3 -o isochromic
//    # takes awhile to compile, because of optimization and stb_image
#include <ctype.h>
#include <fftw3.h>
#include <math.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <readline/readline.h>
#include <readline/history.h>
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
#ifdef _MSC_VER
 #define STBI_MSC_SECURE_CRT
#endif
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"



// all raster data is stored internally with type Pixel:
#ifdef LESS_PRECISION
 typedef float Pixel;
 #define IS_NAN isnanf
 #define fftwx_plan           fftwf_plan
 #define fftwx_plan_r2r_2d    fftwf_plan_r2r_2d
 #define fftwx_execute        fftwf_execute
 #define fftwx_destroy_plan   fftwf_destroy_plan
#else
 typedef double Pixel;
 #define IS_NAN isnan
 #define fftwx_plan           fftw_plan
 #define fftwx_plan_r2r_2d    fftw_plan_r2r_2d
 #define fftwx_execute        fftw_execute
 #define fftwx_destroy_plan   fftw_destroy_plan
#endif



#define MAXVARS 8192
int nVars=0;
typedef struct {
 char *name;
 int width, height;
 Pixel *data;
} Var;
Var vars[MAXVARS] = {0};

#define RESULT_OK    0 //
#define RESULT_ERROR 1 // return values used in a few functions
#define RESULT_EXIT -1 //



void help() {
 puts("Elie's isochromic image generator v0.5");
 puts("Instructions are not fully written yet");
 puts(" ");
 puts("Commands - quick reference:");
 puts(" foo << input.png  # Load data from file (can create new variable 'foo')");
 puts(" bar := foo        # Copy data           (can create new variable 'bar')");
 puts(" bar += foo        # Addition");
 puts(" bar -= foo        # Subtraction");
 puts(" bar *= foo        # Multiplication");
 puts(" bar /= foo        # Division");
 puts(" bar ^= foo        # Exponentiation");
 puts(" bar <= foo        # Max function");
 puts(" bar >= foo        # Min function");
 puts(" bar _= foo        # Fill in any blanks");
 puts(" bar [] foo        # Array lookup");
 puts(" bar :: foo        # Resize image (or create new blank variable 'bar')");
 puts(" bar >> output.png # Save data to file");
 puts(" -- ");
 puts(" vars              # List all variables");
 puts(" help              # Show help info");
 puts(" done              # Quit the program");
 puts(" -- # Special functions:");
 puts(" foo @@ spread     # Smooth out the overflows");
 puts(" foo @@ stats      # Quick statistical info");
 puts(" foo @@ clear      # Delete 'foo', free up memory");
}


Var *find_var(char *name) {
 for (int i=0; i<nVars; i++) if (!strcmp(name, vars[i].name)) return &vars[i];
 return NULL;
}

Var *create_var(char *name, int width, int height) {
 int nBytes = width * height * sizeof(Pixel);
 Var *v = NULL;
 for (int i=0; i<nVars; i++) if (!strcmp(name, vars[i].name)) {
  v = &vars[i];  // variable exists - will be overwritten
  free(v->data); // free existing memory
  break;
 }
 if (!v) {
  if (nVars < MAXVARS) {
   v = &vars[nVars++]; // create new variable
   v->name = strdup(name);
  }
  else printf("Too many variables - can't create another.\n");
 }
 v->data = malloc(nBytes);
 if (v->data) {
  v->width = width;
  v->height = height;
 }
 else {
  printf("Not enough memory for variable's new data (need %d MB)\n", (nBytes+500000)/1000000);
  v->width=0; v->height=0; v=NULL;
 }
 return v;
}







int needsSpread(const Var *v) {
 int size = v->width * v->height;
 for (int i=0; i<size; i++) if (v->data[i] < 0.0 || v->data[i] > 1.0) return 1;
 return 0;
}

volatile int _spread_interrupted = 0;

void _spread_ctrl_c(int sig) {
 signal(SIGINT, SIG_IGN);
 printf("\n\nSpread interrupted by CTRL-C\n\n");
 _spread_interrupted = 1;
}




int spread(Pixel *output, const Pixel *input, int width, int height)
{
 _spread_interrupted = 0;
 if (width<3 || height<3) {
  printf("Can't generate isochromic image - dimensions are too small.\n");
  return RESULT_ERROR;
 }
 int size = width*height;
 for (int i=0; i<size; i++) if (IS_NAN(input[i])) {
  printf("Can't generate isochromic image - data contains blanks (NaNs)\n");
  printf("Fill them in using the _= operator.\n");
  return RESULT_ERROR;
 }
 Pixel sum=0; for (int i=0; i<size; i++) sum += input[i];
 if (sum < 0.0 || sum > size) {
  printf("Can't generate isochromic image - pixels average to %lf.\n", (double)sum/size);
  return RESULT_ERROR;
 }
 Pixel *over = malloc(size*sizeof(Pixel)); // buffer for the overflows
 Pixel *temp = malloc(size*sizeof(Pixel)); // temporary buffer used for box blur
 if (!over || !temp) {
  printf("Not enough memory\n");
  free(over); free(temp);
  return RESULT_ERROR;
 }

 signal(SIGINT, _spread_ctrl_c);
 printf(" Spreading out the overflows - this may take awhile"); fflush(stdout);
 if (output != input) memcpy(output, input, size*sizeof(Pixel));
 printf("...\n");
 int blurSize=1;
 int passes=0;
 while (!_spread_interrupted)
 {
  // transfer the overflows to over[]
  sum=0;int count=0;
  for (int i=0; i<size; i++) {
   if (output[i] > 1.0) {
    over[i] = output[i] - 1.0;
    output[i] = 1.0;
    sum += over[i];  if (over[i] > 0.01) count++;
   }
   else if (output[i] < 0.0) {
    over[i] = output[i];
    output[i] = 0.0;
    sum -= over[i];  if (over[i] < -0.01) count++;
   }
   else over[i] = 0.0;
  }
  printf("  %d overflows, ", count);
  if (count == 0) {
   printf("done.\n");
   break;  // main exit point
  }
  printf("typically by %lf\n", (double)sum/count);
  if (++passes > 128) {
   printf("Too many passes. Not all the overflows are gone, but we have to move on.\n");
   printf("Image won't be perfectly isochromic, but still pretty close.\n");
   break;
  }

  // box-blur the overflows - horizontally (over -> temp)
  printf("   Applying box blur radius %d", blurSize); fflush(stdout);
  Pixel norm = 1.0 / (blurSize*2+1);
  Pixel a=0; for (int i=0; i<=blurSize*2; i++) a += over[i];
  Pixel *sub = &over[0];
  Pixel *add = &over[blurSize*2+1];
  Pixel *write=&temp[blurSize];
  Pixel *end = &temp[size-blurSize];
  for (int i=0; i<blurSize; i++) temp[i] = a*norm; // first pixels
  while (1) {
   *(write++) = a*norm;                             // main pixels (wrap rows)
   if (write == end) break;
   a += *(add++);
   a -= *(sub++);
  }
  for (int i=size-blurSize; i<size; i++) temp[i] = a*norm; // last pixels


  // box-blur the overflows - vertically (temp -> over)
  printf("...\n");
  for (int i=0; i<width && !_spread_interrupted; i++) {  // for each column...
   a=0; for (int j=0; j<=blurSize*2; j++) a += temp[i+j*width];
   sub = &temp[i];
   add = &temp[i + (blurSize*2+1)*width];
   write=&over[i + blurSize*width];
   end = &over[i + size - blurSize*width];
   for (int j=0; j<blurSize; j++) over[i+j*width] = a*norm; // first pixels of column
   while (1) {
    *write = a*norm; write += width;                         // main pixels of column
    if (write >= end) break;
    a += *add; add += width;
    a -= *sub; sub += width;
   }
   for (int j=blurSize; j>0; j--) over[size+i-j*width] = a*norm; // last pixels of column
  }

  // add back the blurred overflows
  if (_spread_interrupted)for (int i=0; i<size; i++) output[i] += temp[i]; // rare case
  else                    for (int i=0; i<size; i++) output[i] += over[i]; // main case

  // blur will be bigger next iteration
  blurSize = blurSize*1.4+1;
  if (blurSize >  width/3) blurSize =  width/3;
  if (blurSize > height/3) blurSize = height/3;
  if (blurSize < 1) blurSize = 1;
 }
 free(temp);
 free(over);
 signal(SIGINT, SIG_DFL);
 if (_spread_interrupted) return RESULT_ERROR;
 return RESULT_OK;
}











int resize(Pixel **data, int oldWidth, int oldHeight, int newWidth, int newHeight, int preserveSum) // the 'data' param is a pointer to a pointer, because it may be modified to point to another chunk of memory with the new size
{
 if (oldWidth==newWidth && oldHeight==newHeight) return RESULT_OK;
 int oldSize = oldWidth * oldHeight;
 int newSize = newWidth * newHeight;
 if (oldSize==0 || newSize==0) return RESULT_ERROR;

 // memory management
 Pixel *freq = malloc(sizeof(Pixel) * (oldSize > newSize ? oldSize : newSize));
 if (  !freq          )  return RESULT_ERROR;
 if (newSize > oldSize) {
  Pixel *nb = realloc(*data, newSize*sizeof(Pixel));
  if (!nb) { free(freq); return RESULT_ERROR; }
  *data = nb;
 }

 // get the frequency data from the image
 fftwx_plan plan = fftwx_plan_r2r_2d(oldHeight, oldWidth, *data, freq, FFTW_REDFT10, FFTW_REDFT10, FFTW_ESTIMATE | FFTW_DESTROY_INPUT);
 fftwx_execute(plan);
 fftwx_destroy_plan(plan);
 if (!preserveSum) for (int i=0; i<oldSize; i++) freq[i] /= 4.0*oldSize; // normalize

 // re-organize the frequency bins, for the new image size
 Pixel *r; Pixel *w; Pixel *end = freq + newSize;
 int minWidth  = oldWidth  < newWidth  ? oldWidth  : newWidth;
 int minHeight = oldHeight < newHeight ? oldHeight : newHeight;
 int diff = newWidth - oldWidth;
 if (diff < 0) { // width decrease: truncate each row of frequencies:
  r = w = freq;
  for (int i=0; i<minHeight; i++) {
   for (int j=0; j<newWidth; j++) *(w++) = *(r++);
   r += -diff;
  }
  while (w<end) *(w++)=0;
 }
 else if (diff > 0) { // width increase: pad each row of frequencies:
  r = freq + oldWidth * minHeight;
  w = freq + newWidth * minHeight;
  for (int i=0; i<minHeight; i++) {
   for (int j=0; j<diff;     j++) *(--w) = 0;
   for (int j=0; j<oldWidth; j++) *(--w) = *(--r);
  }
  w = freq + newWidth * minHeight;
  while (w<end) *(w++)=0;
 }
 else { // same width: just pad the height if needed:
  w = freq + oldSize;
  while (w<end) *(w++)=0;
 }

 // apply a roll-off filter to reduce ringing effects
 const int P=3; // a lower P removes more ringing but could be too blurry
 if (oldWidth != newWidth) {
  int n = minWidth/P;
  Pixel d = 1.0/(n+1);
  for (int i=0; i<minHeight; i++) {
   w = freq + i*newWidth + minWidth - n;
   for (int j=n; j>0; j--) {
    Pixel y=j*d; y=y*y*(3-2*y);// smoothstep
    *(w++) *= y;               // fade out 'n' frequency bins
   }
  }
 }
 if (oldHeight != newHeight) {
  int n = minHeight/P;
  Pixel d = 1.0/(n+1);
  for (int j=n; j>0; j--) {
   w = freq + (minHeight-j)*newWidth;
   Pixel y=j*d; y=y*y*(3-2*y);                // smoothstep
   for (int i=0; i<minWidth; i++) *(w++) *= y;// fade out 'n' frequency bins
  }
 }

 // generate a new image with the truncated/padded/filtered frequency data
 plan = fftwx_plan_r2r_2d(newHeight, newWidth, freq, *data, FFTW_REDFT01, FFTW_REDFT01, FFTW_ESTIMATE | FFTW_DESTROY_INPUT);
 fftwx_execute(plan);
 fftwx_destroy_plan(plan);
 if (preserveSum) for (int i=0; i<newSize; i++) (*data)[i] /= 4.0*newSize; // normalize

 // memory management
 free(freq);
 if (newSize < oldSize) {
  Pixel *nb = realloc(*data, newSize*sizeof(Pixel));
  if (nb) *data = nb;
 }
 return RESULT_OK;
}







int multiply_width(Pixel **data, int width, int height, int scale) {
 if (scale < 1) { printf("Scale out of range\n"); return RESULT_ERROR; }
 if (scale == 1) { printf("Scale=1, no resizing done.\n"); return RESULT_OK; }
 Pixel *nb = realloc(*data, sizeof(Pixel)*width*height*scale);
 if   (!nb) { printf("Not enough memory for resize\n"); return RESULT_ERROR; }
 *data =nb;
 printf("Resizing from %dx%d to %dx%d\n", width, height, width*scale, height);
 Pixel *r = *data + width*height;
 Pixel *w = *data + width*height*scale;
 while (w > *data) { --r; for (int i=0; i<scale; i++) *(--w) = *r; }
 return RESULT_OK;
}


int divide_width(Pixel **data, int width, int height, int scale) {
 if (scale < 1) { printf("Scale out of range\n"); return RESULT_ERROR; }
 if (scale == 1) { printf("Scale=1, no resizing done.\n"); return RESULT_OK; }
 if (width%scale) { printf("Width not divisible by %d. Use the :: resize operator instead.\n", scale); return RESULT_ERROR; }
 printf("Resizing from %dx%d to %dx%d\n", width, height, width/scale, height);
 Pixel *r = *data;  Pixel *w = *data;  Pixel *end = *data + width*height;
 while (r < end) {
  *w = *(r++);
  for (int i=1; i<scale; i++) *w += *(r++);
  w++;
 }
 Pixel *nb = realloc(*data, sizeof(Pixel)*width*height/scale);
 if    (nb) *data = nb;
 return RESULT_OK;
}


int multiply_height(Pixel **data, int width, int height, int scale) {
 if (scale < 1) { printf("Scale out of range\n");  return RESULT_ERROR;}
 if (scale == 1) { printf("Scale=1, no resizing done.\n"); return RESULT_OK; }
 Pixel *nb = realloc(*data, sizeof(Pixel)*width*height*scale);
 if   (!nb) { printf("Not enough memory for resize\n"); return RESULT_ERROR; }
 *data =nb;
 printf("Resizing from %dx%d to %dx%d\n", width, height, width, height*scale);
 Pixel *r = *data + width*height;
 Pixel *w = *data + width*height*scale;
 for (int i=0; i<height; i++) {
  for (int j=0;;) {
   for (int k=0; k<width; k++) *(--w) = *(--r);
   if (++j >= scale) break;
   r += width;
  }
 }
 return RESULT_OK;
}


int divide_height(Pixel **data, int width, int height, int scale) {
 if (scale < 1) { printf("Scale out of range\n"); return RESULT_ERROR; }
 if (scale == 1) { printf("Scale=1, no resizing done.\n"); return RESULT_OK; }
 if (height%scale) { printf("Height not divisible by %d. Use the :: resize operator instead.\n", scale); return RESULT_ERROR; }
 printf("Resizing from %dx%d to %dx%d\n", width, height, width, height/scale);
 Pixel *r = *data;  Pixel *w = *data;  Pixel *end = *data + width*height;
 while (r < end) {
  for (int i=0; i<width; i++) *(w++) = *(r++);
  for (int i=1; i<scale; i++) {
   w -= width;
   for (int j=0; j<width; j++) *(w++) += *(r++);
  }
 }
 Pixel *nb = realloc(*data, sizeof(Pixel)*width*height/scale);
 if    (nb) *data = nb;
 return RESULT_OK;
}








char *fileExtension(char *filename) {
 char *x = filename;
 while (*x) x++;
 while (--x >= filename) if (*x == '.') return x;
 return NULL;
}











int processCommand(char* cmd) { // note: the 'cmd' string is processed destructively.
 // main cursor for parsing
 char* p = cmd;
 // cut off any comments that start with #
 while (*p) {
  if (*p=='#') { *p=0; break; }
  p++;
 }
 // back to beginning
 p = cmd;
 // skip whitespace
 while (*p && isspace(*p)) p++;

 // filter out some cases
 if (*p == 0) return RESULT_OK; // empty line
 if (*p == '?') { help(); return RESULT_OK; }
 if (!isalpha(*p)) {
  printf("Syntax error\n");
  return RESULT_ERROR;
 }

 // main cases...
 char *left = p; // left side of the command (should be a variable's name and that's all)
 while (*p && (isalnum(*p) || *p=='_' || *p=='.')) p++;
 char *leftEnd = p;
 if (leftEnd - left == 4) {
  if (!strncasecmp(left,"done",4)) return RESULT_EXIT;
  if (!strncasecmp(left,"quit",4)) return RESULT_EXIT;
  if (!strncasecmp(left,"exit",4)) return RESULT_EXIT;
  if (!strncasecmp(left,"help",4)) { help(); return RESULT_OK; }
  if (!strncasecmp(left,"vars",4)) {
   int mem=0;
   for (int i=0; i<nVars; i++) {
    printf("%s (%dx%d)\n", vars[i].name, vars[i].width, vars[i].height);
    mem += vars[i].width * vars[i].height * sizeof(Pixel);
   }
   if (nVars) printf("Total memory used: %.1lf MB\n", mem*0.000001);
   else       printf("No variables yet.\n");
   return RESULT_OK;
  }
 }
 while (*p && isspace(*p)) p++;
 // operator characters
 char c0=0, c1=0;
 if (*p) c0 = *(p++);
 if (*p) c1 = *(p++);
 // right side of the command
 while (*p && isspace(*p)) p++;
 if (!*p) { printf("Right side missing\n"); return RESULT_ERROR; }
 char* right = p;
 // find trailing whitespace at the end
 while (*p) p++;
 if (p>cmd) p--;
 while (p>cmd && isspace(*p)) p--;
 if (*p) p++;
 // add string terminators
 *p = 0;       // for string 'right'
 *leftEnd = 0; // for string 'left'






 // operator << (load file)
 if (c0=='<' && c1=='<') {
  char *ext = fileExtension(right);
  if (!strncasecmp(ext, ".data-", 6))
  {
   // case: raw headerless data (type and dimensions are in the file extension)
   char *type  =   ext          + 6;
   char *w=type; while (*w && *w != '-') w++;
   if (!*w || !*(++w)) { printf("File extension is missing dimensions\n"); return RESULT_ERROR; }
   char *h = w;  while (*h && *h != 'x') h++;
   if (!*h || !*(++h)) { printf("File extension is missing dimensions\n"); return RESULT_ERROR; }
   int width  = atoi(w);
   int height = atoi(h);

   // XXX: this implementation assumes that the input file has the same endianness as the system architecture (typically little-endian). Maybe we should we devise some other naming standard for headerless data? Perhaps existing notation could indicate little-endian, and some other notation could indicate big-endian? idk
   FILE *f = fopen(right, "r");
   if (!f) { perror(right); return RESULT_ERROR; }
   Var *v = create_var(left, width, height);
   if (!v) { fclose(f);     return RESULT_ERROR; }
   int result = RESULT_OK;

   #define LOAD_DATA(T, TNAME) { \
    printf("Loading raw headerless "TNAME" data (%dx%d)\n", width, height); \
    T *row = malloc(width * sizeof(T)); \
    if (row) { \
     for (int i=0; i<height; i++) { \
      if (fread(row, sizeof(T), width, f) == 0) { printf("Incomplete file\n"); break; } \
      Pixel *p = &v->data[i*width]; \
      for (int j=0; j<width; j++) p[j] = row[j]; \
     } \
     free(row); \
    } \
    else { \
     printf("Not enough memory\n"); \
     free(v->data); v->data=NULL; v->width=0; v->height=0; \
     result = RESULT_ERROR; \
    } \
   } \

   if /**/ (!strncasecmp(type,"float64-",8)) {LOAD_DATA(double,  "64-bit floating-point")}
   else if (!strncasecmp(type,"float32-",8)) {LOAD_DATA(float,   "32-bit floating-point")}
   else if (!strncasecmp(type,"uint64-", 7)) {LOAD_DATA(uint64_t,"64-bit unsigned integer")}
   else if (!strncasecmp(type,"uint32-", 7)) {LOAD_DATA(uint32_t,"32-bit unsigned integer")}
   else if (!strncasecmp(type,"uint16-", 7)) {LOAD_DATA(uint16_t,"16-bit unsigned integer")}
   else if (!strncasecmp(type,"uint8-",  6)) {LOAD_DATA(uint8_t,  "8-bit unsigned integer")}
   else if (!strncasecmp(type,"int64-",  6)) {LOAD_DATA(int64_t, "64-bit signed integer")}
   else if (!strncasecmp(type,"int32-",  6)) {LOAD_DATA(int32_t, "32-bit signed integer")}
   else if (!strncasecmp(type,"int16-",  6)) {LOAD_DATA(int16_t, "16-bit signed integer")}
   else if (!strncasecmp(type,"int8-",   5)) {LOAD_DATA(int8_t,   "8-bit signed integer")}
   else {
    printf("Unrecognized datatype in file extension\n");
    free(v->data); v->data=NULL; v->width=0; v->height=0;
    result = RESULT_ERROR;
   }
   fclose(f);
   return result; // done loading headerless data
  }

  // other case: image file:
  printf("Loading image file"); fflush(stdout);
  int width=0, height=0, nChannels=1;
  unsigned char *bytes = stbi_load(right, &width, &height, &nChannels, 0);
  if (!bytes) {
   perror(" - failed\n");
   return RESULT_ERROR;
  }
  int size = width * height;
  int result = RESULT_OK;
  if (nChannels == 1) {
   printf(" (%dx%d, mono)\n", width, height);
   Var *v = create_var(left, width, height);
   if (!v) result = RESULT_ERROR;
   else {
    for (int i=0; i<size; i++) v->data[i] = bytes[i]/255.000;
    v->width = width;
    v->height = height;
   }
  }
  else if (nChannels <= 4 && nChannels > 1) {
   char *ch = "rgba";
   printf(" (%dx%d, ", width, height);
   for (int i=0; i<nChannels; i++) putchar(ch[i]); printf(")\n");
   // generate variables' names such as  foo.r  foo.g  foo.b
   int len = strlen(left);
   char *vname = malloc(len+3);   if(!vname){printf("Out of memory\n");return RESULT_ERROR;}
   strcpy(vname, left);
   for (int i=0; i<nChannels; i++) {
    vname[len+0]='.';
    vname[len+1]=ch[i];
    vname[len+2]=0;
    printf("Loading data into '%s'\n", vname);
    Var *v = create_var(vname, width, height);
    if (!v) { result = RESULT_ERROR; break; }
    for (int j=0; j<size; j++) v->data[j] = bytes[i+j*nChannels]/255.000;
   }
   free(vname);
  }
  else printf(" - failed: %d channels not supported\n", nChannels);
  stbi_image_free(bytes);
  return result;
 }








 // operator := (copy data)
 if (c0==':' && c1=='=') {
  // case: right side is a number:
  if (isdigit(right[0]) || right[0]=='.' || right[0]=='-' || right[0]=='+') {
   Pixel number = atof(right);
   Var *lv = find_var(left);
   if (!lv) {
    printf("Unknown variable '%s'\n", left);
    return RESULT_ERROR;
   }
   printf("Filling '%s' (%dx%d) with value %lf\n", left, lv->width, lv->height, (double)number);
   int size = lv->width * lv->height;
   for (int i=0; i<size; i++) lv->data[i] = number;
   return RESULT_OK;
  }
  // case: right side is a variable:
  Var *rv = find_var(right);
  if (!rv) {
   printf("Unknown variable: '%s'\n", right);
   return RESULT_ERROR;
  }
  Var *lv = create_var(left, rv->width, rv->height);
  if (!lv) return RESULT_ERROR;
  printf("Copying '%s' to '%s' (%dx%d)\n", right, left, rv->width, rv->height);
  memcpy(lv->data, rv->data, rv->width * rv->height * sizeof(Pixel));
  return RESULT_OK;
 }








 // operators += -= *= /= ^= <= >= _=
 if (c1=='=' && (c0=='+'||c0=='-'||c0=='*'||c0=='/'||c0=='^'||c0=='<'||c0=='>'||c0=='_')) {
  Var *lv = find_var(left);
  if (!lv) {
   printf("Unknown variable: %s\n", left);
   return RESULT_ERROR;
  }
  if (!isalpha(right[0])) {
   Pixel number = atof(right);
   printf("Applying [%c %lf] to '%s' (%dx%d)\n", c0, (double)number, left, lv->width, lv->height);
   int size = lv->width * lv->height;
   Pixel *data = lv->data;
   if      (c0=='+') for (int i=0; i<size; i++) data[i] += number;
   else if (c0=='-') for (int i=0; i<size; i++) data[i] -= number;
   else if (c0=='*') for (int i=0; i<size; i++) data[i] *= number;
   else if (c0=='/') for (int i=0; i<size; i++) data[i] /= number;
   else if (c0=='^') for (int i=0; i<size; i++) data[i] = pow(data[i], number);
   else if (c0=='<') for (int i=0; i<size; i++) data[i] = data[i] < number ? data[i] : number;
   else if (c0=='>') for (int i=0; i<size; i++) data[i] = data[i] > number ? data[i] : number;
   else if (c0=='_') for (int i=0; i<size; i++) data[i] = IS_NAN(data[i]) ? number : data[i];
   return RESULT_OK;
  }
  Var *rv = find_var(right);
  if (!rv) {
   printf("Undefined variable: %s\n", right);
   return RESULT_ERROR;
  }
  printf("Applying [%c centered '%s'(%dx%d)] to '%s'(%dx%d)\n", c0, right, rv->width, rv->height, left, lv->width, lv->height);
  int width  = lv->width  < rv->width  ? lv->width  : rv->width;
  int height = lv->height < rv->height ? lv->height : rv->height;
  Pixel *lRow = &lv->data[(lv->width-width)/2 + lv->width*((lv->height-height)/2)];
  Pixel *rRow = &rv->data[(rv->width-width)/2 + rv->width*((rv->height-height)/2)];
  for (int i=0; i<height; i++) {
   if      (c0=='+') for (int j=0; j<width; j++) lRow[j] += rRow[j];
   else if (c0=='-') for (int j=0; j<width; j++) lRow[j] -= rRow[j];
   else if (c0=='*') for (int j=0; j<width; j++) lRow[j] *= rRow[j];
   else if (c0=='/') for (int j=0; j<width; j++) lRow[j] /= rRow[j];
   else if (c0=='^') for (int j=0; j<width; j++) lRow[j] = pow(lRow[j], rRow[j]);
   else if (c0=='<') for (int j=0; j<width; j++) lRow[j] = lRow[j] < rRow[j] ? lRow[j] : rRow[j];
   else if (c0=='>') for (int j=0; j<width; j++) lRow[j] = lRow[j] > rRow[j] ? lRow[j] : rRow[j];
   else if (c0=='_') for (int j=0; j<width; j++) lRow[j] = IS_NAN(lRow[j]) ? rRow[j] : lRow[j];
   lRow += lv->width;
   rRow += rv->width;
  }
  return RESULT_OK;
 }








 // operator [] (array mapping)
 if (c0=='[' && c1==']') {
  Var *lv = find_var(left);
  if (!lv) {
   printf("Unknown variable: %s\n", left);
   return RESULT_ERROR;
  }
  Var *rv = find_var(right);
  if (!rv) {
   printf("Unknown variable: %s\n", right);
   return RESULT_ERROR;
  }
  if (rv->width != 1 && rv->height != 1) {
   printf("%s is not a 1-dimensional array\n", right);
   return RESULT_ERROR;
  }
  printf("Replacing the contents of '%s' with corresponding values from array '%s'\n", left, right);
  Pixel *p   =  lv->data;
  Pixel *end = &lv->data[lv->width * lv->height];
  int    n   =           rv->width * rv->height;
  int blanks = 0;
  while (p < end) {
   if (*p >= 0 && *p < n) *p = rv->data[(int)*p];
   else { *p = NAN; blanks++; }
   p++;
  }
  if (blanks) printf(": %d pixels (%d%%) were bad array indexes, so were filled with NaN\n", blanks, (int)(0.5+100.0*blanks/(lv->width*lv->height)));
  return RESULT_OK;
 }








 // operator :: (resize image)
 if (c0==':' && c1==':') {
  int newWidth=0, newHeight=0, preserveSum=0;
  char *rightx = right;
  if (right[0]=='$') { // the '$' preserves pixel sums. Note that the syntax was previously '_' instead, in version 0.5
   rightx++;
   preserveSum=1;
  }
  Var *rv = find_var(rightx);
  if  (rv) { newWidth=rv->width; newHeight=rv->height; }
  else {
   char *p = rightx; // try to parse dimensions from string
   while (*p && !isdigit(*p)) p++;
   newWidth = atoi(p);
   while (*p &&  isdigit(*p)) p++;
   while (*p && !isdigit(*p)) p++;
   newHeight = atoi(p);
   if (!*p) {
    printf("Unknown variable: '%s'\n", rightx);
    return RESULT_ERROR;
   }
  }
  Var *lv = find_var(left);
  if (newWidth<=0 || newHeight<=0) {
   if (lv) printf("Can't scale down '%s' to zero.\nIf you want to clear the data, use:   %s @@ clear\n", left, left);
   else    printf("Can't create variable with 0 in dimensions\n");
   return RESULT_ERROR;
  }
  if (!lv || lv->width<=0 || lv->height<=0) {
   lv = create_var(left, newWidth, newHeight);
   if (lv==NULL) return RESULT_ERROR;
   printf("Creating blank variable '%s' with dimensions %dx%d\n", left, newWidth, newHeight);
   memset(lv->data, 0, newWidth*newHeight*sizeof(Pixel));
   return RESULT_OK;
  }
  printf("Resizing '%s'(%dx%d) to ", left, lv->width, lv->height);
  if (rv) printf("fit '%s'", rightx);
  printf("(%dx%d) - note: may be slow\n", newWidth, newHeight);
  if (preserveSum) printf(" + preserving the sum of pixels\n");
  int result = resize(&lv->data, lv->width, lv->height, newWidth, newHeight, preserveSum);
  if (result==RESULT_OK) { lv->width=newWidth; lv->height=newHeight; }
  else printf("Failed (probably out of memory)\n");
  return result;
 }








 // operator >> (save file)
 if (c0=='>' && c1=='>') {
  int n=0;
  Var *lv[4] = {0};
  lv[0] = find_var(left);
  if (lv[0] && lv[0]->width && lv[0]->height) n = 1;
  else {
   // try multiple channels - for variable 'foo', look for 'foo.r', 'foo.g', 'foo.b', 'foo.a'
   int len = strlen(left);
   char *vname = malloc(len+3);  if(!vname){printf("Out of memory\n");return RESULT_ERROR;}
   strcpy(vname, left);
   char *ch = "rgba";
   do {
    vname[len+0]='.';
    vname[len+1]=ch[n];
    vname[len+2]=0;
    lv[n] = find_var(vname);
   } while (lv[n] && lv[n]->data &&
            lv[n]->width == lv[0]->width &&
            lv[n]->height == lv[0]->height &&
            ++n < 4);
   free(vname);
   if (n==0) {
    printf("Undefined variable '%s'\n", left);
    return RESULT_ERROR;
   }
  }
  int size = lv[0]->width * lv[0]->height;
  unsigned char *bytes = malloc(n*size*sizeof(unsigned char));
  if (!bytes) {
   printf("Not enough memory.\n");
   return RESULT_ERROR;
  }
  if (n==1) printf("'%s'(%dx%d) will generate monochrome file: %s\n", left, lv[0]->width, lv[0]->height, right);
  else {
   printf("Will generate file %s\nfrom channels", right);
   for (int i=0; i<n; i++) printf(" '%s' ", lv[i]->name);
   printf(" (%dx%d)\n", lv[0]->width, lv[0]->height);
  }
  for (int i=0; i<n; i++) {
   Pixel *data = lv[i]->data;
   int highs=0, lows=0;
   for (int j=0; j<size; j++) {
    if      (data[j] < 0.0) { bytes[j*n+i] = 0;   lows++; }
    else if (data[j] > 1.0) { bytes[j*n+i] = 255; highs++; }
    else                    { bytes[j*n+i] = data[j]*255.999; }
   }
   if (highs || lows) {
    printf("  Note: '%s' contains overflows(%d pixels are >1)(%d pixels are <0)\n", lv[i]->name, highs, lows);
    printf("    Image will not be isochromic.\n    If you want it to be, you need to first do:\n");
    printf("      %s @@ spread\n    (which would alter the contents of '%s').\n", lv[i]->name, lv[i]->name);
   }
  }
  printf("\n Writing %s\n", right);
  int wrote=0;
  char *ext = fileExtension(right);
  if (!strcasecmp(ext,".jpg") || !strcasecmp(ext,".jpeg"))
       wrote = stbi_write_jpg(right, lv[0]->width, lv[0]->height, n, bytes, 100);
  else wrote = stbi_write_png(right, lv[0]->width, lv[0]->height, n, bytes, lv[0]->width * n);
  free(bytes);
  if (!wrote) { printf("Can't write file\n"); return RESULT_ERROR; }
  return RESULT_OK;
 }








 // operator @@ (special functions)
 if (c0=='@' && c1=='@') {
  Var *lv = find_var(left);
  if (!lv) {
   printf("Unknown variable '%s'\n", left);
   return RESULT_ERROR;
  }
  if (!strcasecmp(right, "spread")) {
   return spread(lv->data, lv->data, lv->width, lv->height);
  }
  if (!strcasecmp(right, "clear") || !strcasecmp(right, "delete") || !strcasecmp(right, "free")) {
   printf("Clearing variable '%s'\n", left);
   free(lv->data); lv->data = NULL;
   lv->width = lv->height = 0;
   return RESULT_OK;
  }
  if (!strcasecmp(right, "stats") || !strcasecmp(right, "statistics")) {
   Pixel *data = lv->data;
   int    size = lv->width * lv->height;
   printf("%s\n Dimensions: %d by %d\n", left, lv->width, lv->height);
   Pixel sum=0; int count=0;
   for (int i=0; i<size; i++) if (!IS_NAN(data[i])) { sum += data[i]; count++; }
   printf(" Sum: %lf\n", (double)sum);
   Pixel avg=sum/count; Pixel std=0;
   printf(" Average (Mean): %lf\n", (double)avg);
   for (int i=0; i<size; i++) if (!IS_NAN(data[i])) std += (data[i]-avg)*(data[i]-avg);
   std = sqrt(std/count);
   printf(" Standard Deviation: (+/-) %lf\n", (double)std);
   Pixel lowest=INFINITY; int where=0;
   for (int i=0; i<size; i++) if (data[i]<lowest)  { lowest=data[i];  where=i; }
   printf(" Minimum: %lf at [%d,%d]\n", (double)lowest, where % lv->width, where / lv->width);
   Pixel highest=-INFINITY; where=0;
   for (int i=0; i<size; i++) if (data[i]>highest) { highest=data[i]; where=i; }
   printf(" Maximum: %lf at [%d,%d]\n", (double)highest, where % lv->width, where / lv->width);
   return RESULT_OK;
  }

  if (!strncasecmp(right,"pixel",5)) {
   int x,y; double value;
   if (sscanf(right+5," [ %d , %d ] = %lf ",&x,&y,&value) != 3) {
    printf("pixel: Syntax error\n");
    return RESULT_ERROR;
   }
   if (x < 0 || x >= lv->width || y < 0 || y >= lv->height) {
    printf("Pixel location out of range\n");
    return RESULT_ERROR;
   }
   printf("Setting pixel in '%s' at [%d,%d] to value %lf\n",left,x,y,value);
   lv->data[x + y*lv->width] = value;
   return RESULT_OK;
  }


  if (!strncasecmp(right,"width",5) || !strncasecmp(right,"$width",6)) {
   if (lv->width<=0 || lv->height<=0) {
    printf("Can't scale an empty variable\n");
    return RESULT_ERROR;
   }
   char *p = &right[5];  while (*p && *p != '*' && *p != '/') p++;
   if (!*p) {
    printf("width: missing operand\nSyntax example: if you want to stretch the width by 2:   %s @@ width*2", left);
    return RESULT_ERROR;
   }
   Pixel f = atof(p+1);  int scale = f;
   if (scale != f) {
    printf("This scaling method can only multiply or divide by an integer.\n");
    printf("Try instead:\n  %s :: %dx%d\n", left, (int)(*p=='/' ? 0.5+lv->width/f : 0.5+lv->width*f), lv->height);
    return RESULT_ERROR;
   }
   printf("'%s': ", left); fflush(stdout);
   if (*p == '/') {
    int result = divide_width(&lv->data, lv->width, lv->height, scale);
    if (result != RESULT_OK) return result;
    lv->width /= scale;
   }
   if (*p == '*') {
    int result = multiply_width(&lv->data, lv->width, lv->height, scale);
    if (result != RESULT_OK) return result;
    lv->width *= scale;
   }
   if (right[0]=='$') printf(" + preserving the sum of pixels\n");
   if((right[0]=='$') != (*p=='/')) for (int i=0; i < lv->width*lv->height; i++) lv->data[i] /= scale;
   return RESULT_OK;
  }


  if (!strncasecmp(right,"height",6) || !strncasecmp(right,"$height",7)) {
   if (lv->width<=0 || lv->height<=0) {
    printf("Can't scale an empty variable\n");
    return RESULT_ERROR;
   }
   char *p = &right[6];  while (*p && *p != '*' && *p != '/') p++;
   if (!*p) {
    printf("height: missing operand\nSyntax example: if you want to stretch the height by 2:   %s @@ height*2", left);
    return RESULT_ERROR;
   }
   Pixel f = atof(p+1);  int scale = f;
   if (scale != f) {
    printf("This scaling method can only multiply or divide by an integer.\n");
    printf("Try instead:\n  %s :: %dx%d\n", left, lv->width, (int)(*p=='/' ? 0.5+lv->height/f : 0.5+lv->height*f));
    return RESULT_ERROR;
   }
   printf("'%s': ", left); fflush(stdout);
   if (*p == '/') {
    int result = divide_height(&lv->data, lv->width, lv->height, scale);
    if (result != RESULT_OK) return result;
    lv->height /= scale;
   }
   if (*p == '*') {
    int result = multiply_height(&lv->data, lv->width, lv->height, scale);
    if (result != RESULT_OK) return result;
    lv->height *= scale;
   }
   if (right[0]=='$') printf(" + preserving the sum of pixels\n");
   if((right[0]=='$') != (*p=='/')) for (int i=0; i < lv->width*lv->height; i++) lv->data[i] /= scale;
   return RESULT_OK;
  }


  if (!strcasecmp(right, "ln") || !strncasecmp(right, "natural_log", 11)) {
   printf("Applying the natural logarithm function, to pixels in '%s'\n", left);
   int size = lv->width * lv->height;
   for (int i=0; i<size; i++) lv->data[i] = log(lv->data[i]);
   return RESULT_OK;
  }
  if (!strcasecmp(right, "e") || !strcasecmp(right, "exp") || !strcasecmp(right, "exponential")) {
   printf("Applying the exponential function (e^x), to pixels in '%s'\n", left);
   int size = lv->width * lv->height;
   for (int i=0; i<size; i++) lv->data[i] = exp(lv->data[i]);
   return RESULT_OK;
  }

  if (!strcasecmp(right, "neg") || !strcasecmp(right, "negative")) {
   printf("Inverting colors in '%s'\n", left);
   int size = lv->width * lv->height;
   for (int i=0; i<size; i++) lv->data[i] = 1.0 - lv->data[i];
   return RESULT_OK;
  }
  if (!strcasecmp(right, "01") || !strcasecmp(right, "clamp")) {
   printf("Clamping numbers in '%s' to range '0 to 1'\n", left);
   int size = lv->width * lv->height;
   for (int i=0; i<size; i++) {
    if      (lv->data[i] < 0.0) lv->data[i] = 0.0;
    else if (lv->data[i] > 1.0) lv->data[i] = 1.0;
   }
   return RESULT_OK;
  }
  if (!strcasecmp(right, "smoothstep") || !strcasecmp(right, "smooth_clamp")) {
   printf("Applying smoothstep function (0 to 1) to numbers in '%s'\n", left);
   int size = lv->width * lv->height;
   for (int i=0; i<size; i++) {
    Pixel y = lv->data[i];
    if      (y <= 0.0) lv->data[i] = 0.0;
    else if (y >= 1.0) lv->data[i] = 1.0;
    else {
     y *= y*(3-2*y);
     lv->data[i] = y;
    }
   }
  }
  if (!strcasecmp(right, "density_to_quantity") || !strcasecmp(right, "d2q")
  ||  !strcasecmp(right, "quantity_to_density") || !strcasecmp(right, "q2d"))
  {
   printf("Treating '%s' like an equirectangular world map\n", left);
   int divide = (right[0]=='q');
   printf("and %s each pixel by its land area (km^2)", divide?"dividing":"multiplying");
   fflush(stdout);
   const Pixel EARTH_SURFACE = 510072000; // square kilometers
   Pixel increment = M_PI / lv->height;
   Pixel firstRow = -M_PI_2 + increment/2;
   for (int i=0; i < lv->height; i++) {
    Pixel latitude = firstRow + i * increment;
    Pixel land = cos(latitude) * EARTH_SURFACE * M_PI_2 / lv->height / lv->width; // land in km^2 per pixel in row
    if (divide) land = 1.0 / land;
    Pixel *row = &lv->data[i * lv->width];
    for (int j=0; j < lv->width; j++) row[j] *= land;
   }
   printf(" - done\n");
   return RESULT_OK;
  }
  printf("Unknown special function: %s\n", right);
  return RESULT_ERROR;
 }


 if (c0) printf("Unknown operator: %c%c\n", c0, c1);
 else    printf("Syntax error\n");
 return RESULT_ERROR; //
}                     // end of function processCommand()






void done() {
 for (int i=0; i<nVars; i++) {
  free(vars[i].name);
  free(vars[i].data);
 }
 nVars=0;
}







#define HISTORY_FILE ".isochromic_history"
#define HISTORY_MAX_SAVED 2000

int main() {
 using_history();
 read_history(HISTORY_FILE);
 printf("Welcome. For help info, type:   help\n");
 int result = RESULT_OK;
 while (result != RESULT_EXIT) {
  char *line = readline("() ");
  if (!line) break;
  if (*line) add_history(line);
  result = processCommand(line);
  free(line);
 }
 done();
 stifle_history(HISTORY_MAX_SAVED);
 write_history(HISTORY_FILE);
 return 0;
}