/* Copyright 2022-2026 Kurt Nienhaus
 *
 * This file is part of VgaGames.
 * VgaGames is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 * VgaGames is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * You should have received a copy of the GNU General Public License
 * along with VgaGames.  If not, see <http://www.gnu.org/licenses/>.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <errno.h>
#include "image.h"

/* PNM-Bilder
 * ppm (Farb-Bild)
 * ===
 *   Kommentar ab '#' bis Zeilenende
 *   - raw (kann mehrere in einer Datei haben)
 *     P6
 *     <whitespaces>
 *     <Breite in dez.ASCII>
 *     <whitespaces>
 *     <Hoehe in dez.ASCII>
 *     <whitespaces>
 *     <maximaler Farbwert, kleiner 65536>
 *     <EIN newline oder whitespace>
 *     <Pixel: Breite mal Hoehe: je ein oder zwei Byte(s ab maxval 256 mit MSB+LSB) mit RR, mit GG, mit BB>
 *
 *   - plain
 *     P3
 *     <whitespaces>
 *     <Breite in dez.ASCII>
 *     <whitespaces>
 *     <Hoehe in dez.ASCII>
 *     <whitespaces>
 *     <maximaler Farbwert, kleiner 65536>
 *     <EIN newline oder whitespace>
 *     <Pixel: Breite mal Hoehe: je eine ASCII-Zahl mit RR, mit GG, mit BB,
 *             jede mit <whitespaces> getrennt, max Zeilenlaenge solltesein 70 Bytes>
 *
 *
 * pgm (Grau-Bild)
 * ===
 *   Kommentar ab '#' bis Zeilenende
 *   - raw (kann mehrere in einer Datei haben)
 *     P5
 *     <whitespaces>
 *     <Breite in dez.ASCII>
 *     <whitespaces>
 *     <Hoehe in dez.ASCII>
 *     <whitespaces>
 *     <maximaler Grauwert, kleiner 65536>
 *     <EIN newline oder whitespace>
 *     <Pixel: Breite mal Hoehe: je ein oder zwei Byte(s ab maxval 256 mit MSB+LSB) mit Grauwert (0=schwarz und maxval=weiss)>
 *
 *   - plain
 *     P2
 *     <whitespaces>
 *     <Breite in dez.ASCII>
 *     <whitespaces>
 *     <Hoehe in dez.ASCII>
 *     <whitespaces>
 *     <maximaler Farbwert, kleiner 65536>
 *     <EIN newline oder whitespace>
 *     <Pixel: Breite mal Hoehe: je eine ASCII-Zahl mit Grauwert (0=schwarz und maxval=weiss),
 *             jede mit <whitespaces> getrennt, max Zeilenlaenge solltesein 70 Bytes>
 *
 *
 * pbm (Schwarz-Weiss-Bild)
 * ===
 *   Kommentar ab '#' bis Zeilenende
 *   - raw (kann mehrere in einer Datei haben)
 *     P4
 *     <whitespaces>
 *     <Breite in dez.ASCII>
 *     <whitespaces>
 *     <Hoehe in dez.ASCII>
 *     <EIN newline oder whitespace>
 *     <Pixel: Breite mal Hoehe: je ein Bit (1=schwarz und 0=weiss) von MSB-zu-LSB,
 *             je Zeile: wenn Breite erledigt und letztes Byte noch Bits uebrig hat, wird mit 0 (LSB) aufgefuellt>
 *
 *   - plain
 *     P1
 *     <whitespaces>
 *     <Breite in dez.ASCII>
 *     <whitespaces>
 *     <Hoehe in dez.ASCII>
 *     <EIN newline oder whitespace>
 *     <Pixel: Breite mal Hoehe: je eine ASCII-Zahl (1=schwarz und 0=weiss),
 *             keine <whitespaces> benoetigt, max Zeilenlaenge solltesein 70 Bytes>
 *
 *
 * pam (verschieden)
 * ===
 *   Kommentar ab '#' bis Zeilenende
 *   - raw
 *     P7
 *     WIDTH <Breite in dez.ASCII>
 *     HEIGHT <Hoehe in dez.ASCII>
 *     DEPTH <3 bei RGB, 4 bei RGB_ALPHA>
 *     MAXVAL <maximaler Farbwert, kleiner 65536>
 *     TUPLTYPE <RGB oder RGB_ALPHA>
 *     ENDHDR
 *     <Pixel: Breite mal Hoehe: je ein oder zwei Byte(s ab maxval 256 mit MSB+LSB) mit RR, mit GG, mit BB, evtl. mit AA>
 */

struct VG_Image * vg4_image_loadfile(const char *);
struct VG_Image * vg4_image_loadmem(char *, size_t);
VG_BOOL vg4_image_savefile(const struct VG_Image *, const char *, VG_BOOL);
VG_BOOL vg4_image_tobase64(const struct VG_Image *, VG_BOOL, char **, size_t *);

static int read_nr(char **, const char *, const char *);
static int read_p7header(char **, const char *, const char *, int *, int *, int *);
static void readpnm(int, char **, const char *, int, int, int, const char *, struct VG_Image *);


/* read number as ASCII-string */
static int
read_nr(char **ptpos, const char *ptend, const char *filename)
{
  char *pstart;
  int nr;

  if (ptpos == NULL || *ptpos == NULL) { return 0; }
  if (ptend == NULL) { *ptpos = NULL; return 0; }
  if (filename == NULL) { filename = "[from memory]"; }
  if (*ptpos >= ptend) {
    outerr("reading \"%s\": end of file/memory reached", filename);
    *ptpos = NULL;
    return 0;
  }

  nr = 0;
  pstart = *ptpos;
  while (*ptpos < ptend) {
    if ((*ptpos)[0] < '0' || (*ptpos)[0] > '9') { break; }
    (*ptpos)++;
  }
  if (pstart < *ptpos) {
    nr = (int)SML3_antoi(pstart, (size_t)(*ptpos - pstart));
  } else {
    outerr("reading \"%s\": invalid data character", filename);
    *ptpos = NULL;
    return 0;
  }
  return nr;
} /* Ende read_nr */


/* read header of P7, return PNM-number (6 or 7) or 0 */
static int
read_p7header(char **ptpos, const char *ptend, const char *filename, int *width, int *height, int *maxval)
{
  const char *pheader;
  char *pnewline;
  int pnm_nr;

  pnm_nr = 0;
  if (width == NULL || height == NULL || maxval == NULL) { return 0; }
  *width = *height = *maxval = 0;
  if (ptpos == NULL || *ptpos == NULL) { return 0; }
  if (ptend == NULL) { *ptpos = NULL; return 0; }
  if (filename == NULL) { filename = "[from memory]"; }
  if (*ptpos >= ptend) {
    outerr("reading \"%s\": end of file/memory reached", filename);
    *ptpos = NULL;
    return 0;
  }

nxt_hdr:
  /* find newline */
  pnewline = *ptpos;
  while (pnewline < ptend) {
    if (*pnewline == '\n') { break; }
    pnewline++;
  }
  if (pnewline == ptend) { outerr("reading \"%s\": EOF in header", filename); *ptpos = NULL; return 0; }

  /* get header-key */
  pheader = NULL;
  if (*ptpos <= pnewline - 5 && strncmp(*ptpos, "WIDTH", 5) == 0) {
    (*ptpos) += 5;
    pheader = "WIDTH";
  } else if (*ptpos <= pnewline - 6 && strncmp(*ptpos, "HEIGHT", 6) == 0) {
    (*ptpos) += 6;
    pheader = "HEIGHT";
  } else if (*ptpos <= pnewline - 5 && strncmp(*ptpos, "DEPTH", 5) == 0) {
    (*ptpos) += 5;
    pheader = "DEPTH";
  } else if (*ptpos <= pnewline - 6 && strncmp(*ptpos, "MAXVAL", 6) == 0) {
    (*ptpos) += 6;
    pheader = "MAXVAL";
  } else if (*ptpos <= pnewline - 8 && strncmp(*ptpos, "TUPLTYPE", 8) == 0) {
    (*ptpos) += 8;
    pheader = "TUPLTYPE";
  } else if (*ptpos <= pnewline - 6 && strncmp(*ptpos, "ENDHDR", 6) == 0) {
    (*ptpos) += 6;
    pheader = "ENDHDR";
  }

  if (pheader != NULL) {
    while (*ptpos < pnewline) {
      if (strchr(VGI_SPACE, (int)*ptpos[0]) == NULL) { break; }
      (*ptpos)++;
    }
    if (strcmp(pheader, "TUPLTYPE") == 0) {
      if (*ptpos <= pnewline - 9 && strncmp(*ptpos, "RGB_ALPHA", 9) == 0) {
        pnm_nr = 7;
        (*ptpos) += 9;
      } else if (*ptpos <= pnewline - 3 && strncmp(*ptpos, "RGB", 3) == 0) {
        pnm_nr = 6;
        (*ptpos) += 3;
      } else {
        outerr("reading \"%s\": TUPLTYPE value not supported", filename);
        *ptpos = NULL;
        return 0;
      }
    } else if (*ptpos < pnewline && strcmp(pheader, "ENDHDR") != 0) {
      int nr = read_nr(ptpos, pnewline, filename);
      if (*ptpos == NULL) { return 0; }
      if (strcmp(pheader, "WIDTH") == 0) {
        *width = nr;
        if (*width <= 0) { outerr("loading \"%s\": width invalid: %d", filename, *width); return 0; }
      } else if (strcmp(pheader, "HEIGHT") == 0) {
        *height = nr;
        if (*height <= 0) { outerr("loading \"%s\": height invalid: %d", filename, *height); return 0; }
      } else if (strcmp(pheader, "MAXVAL") == 0) {
        *maxval = nr;
        if (*maxval < 1 || *maxval > 65535) { outerr("loading \"%s\": maxval invalid: %d", filename, *maxval); return 0; }
      } else if (strcmp(pheader, "DEPTH") == 0) {
        ;
      }
    }
  }

  *ptpos = pnewline + 1;
  if (*ptpos == ptend) { outerr("reading \"%s\": EOF in header", filename); *ptpos = NULL; return 0; }

  if (pheader == NULL || strcmp(pheader, "ENDHDR") != 0) { goto nxt_hdr; }

  return pnm_nr;
} /* Ende read_p7header */


/* read pnm data */
static void
readpnm(int pnm_nr, char **ptpos, const char *ptend, int width, int height, int maxval, const char *filename, struct VG_Image *imgp)
{
  int anzdata, momdata;

  if (filename == NULL) { filename = "[from memory]"; }

  anzdata = width * height;
  momdata = 0;

  if (pnm_nr == 1) {  /* P1 */
    VG_BOOL isset;
    struct VG_PixelColor pxcol0, pxcol1;
    GET_RGBA(VG_COLOR_WHITE, &pxcol0.r, &pxcol0.g, &pxcol0.b, &pxcol0.a);
    GET_RGBA(VG_COLOR_BLACK, &pxcol1.r, &pxcol1.g, &pxcol1.b, &pxcol1.a);
    while (momdata < anzdata) {
      vg4_intern_skip_ws((const char **)ptpos, ptend);
      if (*ptpos == ptend) { return; }
      if ((*ptpos)[0] == '0') {
        isset = VG_FALSE;
      } else if ((*ptpos)[0] == '1') {
        isset = VG_TRUE;
      } else {
        outerr("reading \"%s\": invalid data character", filename);
        *ptpos = NULL;
        return;
      }
      if (isset) {
        imgp->img.pixels[momdata] = pxcol1;
      } else {
        imgp->img.pixels[momdata] = pxcol0;
      }
      (*ptpos)++;
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }

  } else if (pnm_nr == 2) {  /* P2 */
    int nr;
    struct VG_PixelColor pxcol;
    while (momdata < anzdata) {
      vg4_intern_skip_ws((const char **)ptpos, ptend);
      if (*ptpos == ptend) { return; }
      nr = read_nr(ptpos, ptend, filename);
      if (*ptpos == NULL) { return; }
      nr = nr * 256 / (maxval + 1);
      pxcol.r = pxcol.g = pxcol.b = nr;
      pxcol.a = 255;
      imgp->img.pixels[momdata] = pxcol;
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }

  } else if (pnm_nr == 3) {  /* P3 */
    int nr, i1;
    struct VG_PixelColor pxcol;
    while (momdata < anzdata) {
      for (i1 = 1; i1 <= 3; i1++) {
        vg4_intern_skip_ws((const char **)ptpos, ptend);
        if (*ptpos == ptend) { return; }
        nr = read_nr(ptpos, ptend, filename);
        if (*ptpos == NULL) { return; }
        nr = nr * 256 / (maxval + 1);
        switch(i1) {
          case 1: pxcol.r = nr; break;
          case 2: pxcol.g = nr; break;
          case 3: pxcol.b = nr; break;
        }
      }
      pxcol.a = 255;
      imgp->img.pixels[momdata] = pxcol;
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }

  } else if (pnm_nr == 4) {  /* P4 */
    VG_BOOL isset;
    struct VG_PixelColor pxcol0, pxcol1;
    int w1, bitpos, ival;
    GET_RGBA(VG_COLOR_WHITE, &pxcol0.r, &pxcol0.g, &pxcol0.b, &pxcol0.a);
    GET_RGBA(VG_COLOR_BLACK, &pxcol1.r, &pxcol1.g, &pxcol1.b, &pxcol1.a);
    w1 = 0;
    bitpos = 7;
    while (momdata < anzdata) {
      if (*ptpos == ptend) {
        outerr("reading \"%s\": end of file/memory reached", filename);
        *ptpos = NULL;
        return;
      }
      ival = ((*ptpos)[0] & (1 << bitpos));
      if (ival) { isset = VG_TRUE; } else { isset = VG_FALSE; }
      if (isset) {
        imgp->img.pixels[momdata] = pxcol1;
      } else {
        imgp->img.pixels[momdata] = pxcol0;
      }
      if (++w1 == width || --bitpos < 0) {
        (*ptpos)++;
        bitpos = 7;
        if (w1 == width) { w1 = 0; }
      }
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }

  } else if (pnm_nr == 5) {  /* P5 */
    int nr;
    struct VG_PixelColor pxcol;
    while (momdata < anzdata) {
      if (*ptpos == ptend) {
        outerr("reading \"%s\": end of file/memory reached", filename);
        *ptpos = NULL;
        return;
      }
      nr = (unsigned char)(*ptpos)[0];
      if (maxval > 255) {
        nr <<= 8;
        (*ptpos)++;
        if (*ptpos == ptend) {
          outerr("reading \"%s\": end of file/memory reached", filename);
          *ptpos = NULL;
          return;
        }
        nr |= (unsigned char)(*ptpos)[0];
      }
      nr = nr * 256 / (maxval + 1);
      pxcol.r = pxcol.g = pxcol.b = nr;
      pxcol.a = 255;
      imgp->img.pixels[momdata] = pxcol;
      (*ptpos)++;
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }

  } else if (pnm_nr == 6) {  /* P6 */
    int nr, i1;
    struct VG_PixelColor pxcol;
    while (momdata < anzdata) {
      for (i1 = 1; i1 <= 3; i1++) {
        if (*ptpos == ptend) {
          outerr("reading \"%s\": end of file/memory reached", filename);
          *ptpos = NULL;
          return;
        }
        nr = (unsigned char)(*ptpos)[0];
        if (maxval > 255) {
          nr <<= 8;
          (*ptpos)++;
          if (*ptpos == ptend) {
            outerr("reading \"%s\": end of file/memory reached", filename);
            *ptpos = NULL;
            return;
          }
          nr |= (unsigned char)(*ptpos)[0];
        }
        nr = nr * 256 / (maxval + 1);
        switch(i1) {
          case 1: pxcol.r = nr; break;
          case 2: pxcol.g = nr; break;
          case 3: pxcol.b = nr; break;
        }
        (*ptpos)++;
      }
      pxcol.a = 255;
      imgp->img.pixels[momdata] = pxcol;
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }

  } else if (pnm_nr == 7) {  /* P7 */
    int nr, i1;
    struct VG_PixelColor pxcol;
    while (momdata < anzdata) {
      for (i1 = 1; i1 <= 4; i1++) {
        if (*ptpos == ptend) {
          outerr("reading \"%s\": end of file/memory reached", filename);
          *ptpos = NULL;
          return;
        }
        nr = (unsigned char)(*ptpos)[0];
        if (maxval > 255) {
          nr <<= 8;
          (*ptpos)++;
          if (*ptpos == ptend) {
            outerr("reading \"%s\": end of file/memory reached", filename);
            *ptpos = NULL;
            return;
          }
          nr |= (unsigned char)(*ptpos)[0];
        }
        nr = nr * 256 / (maxval + 1);
        switch(i1) {
          case 1: pxcol.r = nr; break;
          case 2: pxcol.g = nr; break;
          case 3: pxcol.b = nr; break;
          case 4: pxcol.a = nr; break;
        }
        (*ptpos)++;
      }
      imgp->img.pixels[momdata] = pxcol;
      momdata++;
    }
    if (*ptpos < ptend) { vg4_intern_skip_ws((const char **)ptpos, ptend); }
  }

  if (momdata < anzdata) {
    outerr("reading \"%s\": invalid format", filename);
    *ptpos = NULL;
  }
} /* Ende readpnm */


/* vg4_image_loadfile:
 * load image from file, checking format before
 * @param filename  filename to load
 * @return  image or NULL = error
 */
struct VG_Image *
vg4_image_loadfile(const char *filename)
{
  FILE *ffp;
  struct stat sbuf;
  char *memptr;
  size_t memsize;
  struct VG_Image *imgp;

  if (filename == NULL || *filename == '\0') {
    outerr("loading file: no filename");
    return NULL;
  }

  ffp = fopen(filename, "r");
  if (ffp == NULL) { outerr("loading \"%s\": %s", filename, strerror(errno)); return NULL; }

  if (fstat(fileno(ffp), &sbuf) < 0) {
    outerr("loading \"%s\": fstat: %s", filename, strerror(errno));
    fclose(ffp);
    return NULL;
  }

  memsize = (size_t)sbuf.st_size;
  if (memsize < 4) {
    outerr("loading \"%s\": file too short", filename);
    fclose(ffp);
    return NULL;
  }

  memptr = SML3_malloc(memsize);
  if (fread(memptr, 1, memsize, ffp) != memsize) {
    outerr("loading \"%s\": file too short", filename);
    fclose(ffp);
    return NULL;
  }

  fclose(ffp);

  imgp = vg4_image_loadmem(memptr, memsize);

  free(memptr);

  return imgp;
} /* Ende vg4_image_loadfile */


/* vg4_image_loadmem:
 * load image from memory, checking format before
 * @param memptr   memory
 * @param memsize  size of memory in bytes
 * @return  image or NULL = error
 */
struct VG_Image *
vg4_image_loadmem(char *memptr, size_t memsize)
{
  const char *filename = "[from memory]";
  char *ptpos, *ptend;
  struct VG_Image *imgp;

  if (memptr == NULL || memsize == 0) {
    outerr("loading from memory: no memory-pointer");
    return NULL;
  }

  if (memsize < 4) {
    outerr("loading from memory: memory too short");
    return NULL;
  }

  ptpos = memptr;
  ptend = memptr + memsize;

  if (ptpos[0] == 'P' && ptpos[1] >= '1' && ptpos[1] <= '6'
      && ptpos[2] != '\0' && strchr(VGI_WSPACE, (int)ptpos[2]) != NULL) {  /* pnm */
    int pnm_nr, width, height, maxval;

    pnm_nr = ptpos[1] - '0';
    ptpos = memptr + 3;

    /* read width */
    vg4_intern_skip_ws((const char **)&ptpos, ptend);
    if (ptpos == ptend) { return NULL; }
    width = read_nr(&ptpos, ptend, filename);
    if (ptpos == NULL) { return NULL; }
    if (width <= 0) {
      outerr("loading \"%s\": width invalid: %d", filename, width);
      return NULL;
    }

    /* read height */
    vg4_intern_skip_ws((const char **)&ptpos, ptend);
    if (ptpos == ptend) { return NULL; }
    height = read_nr(&ptpos, ptend, filename);
    if (ptpos == NULL) { return NULL; }
    if (height <= 0) {
      outerr("loading \"%s\": height invalid: %d", filename, height);
      return NULL;
    }

    /* read maxval */
    maxval = 0;
    if (pnm_nr != 1 && pnm_nr != 4) {
      vg4_intern_skip_ws((const char **)&ptpos, ptend);
      if (ptpos == ptend) { return NULL; }
      maxval = read_nr(&ptpos, ptend, filename);
      if (ptpos == NULL) { return NULL; }
      if (maxval < 1 || maxval > 65535) {
        outerr("loading \"%s\": maxval invalid: %d", filename, maxval);
        return NULL;
      }
    }

    /* read one whitespace */
    if (ptpos == ptend || ptpos[0] == '\0' || strchr(VGI_WSPACE, (int)ptpos[0]) == NULL) {
      outerr("reading \"%s\": no whitespace at byte %lu", filename, (long)(size_t)(ptpos - memptr));
      return NULL;
    }
    if (++ptpos == ptend) {
      outerr("loading \"%s\": memory too short", filename);
      return NULL;
    }

    imgp = vg4_image_create_nolist(width, height);
    imgp->img.hasopq = VG_FALSE;
    imgp->img.isclear = VG_FALSE;
    readpnm(pnm_nr, &ptpos, ptend, width, height, maxval, filename, imgp);
    if (ptpos == NULL) { vg4->image->destroy(imgp); return NULL; }

  } else if (ptpos[0] == 'P' && ptpos[1] == '7'
      && ptpos[2] != '\0' && strchr(VGI_WSPACE, (int)ptpos[2]) != NULL) {  /* pam */
    int pnm_nr, width, height, maxval;

    ptpos = memptr + 3;
    vg4_intern_skip_ws((const char **)&ptpos, ptend);
    if (ptpos == ptend) { return NULL; }

    pnm_nr = read_p7header(&ptpos, ptend, filename, &width, &height, &maxval);
    if (ptpos == NULL || pnm_nr == 0) { return NULL; }

    imgp = vg4_image_create_nolist(width, height);
    imgp->img.hasopq = (pnm_nr == 6 ? VG_FALSE : VG_TRUE);
    imgp->img.isclear = VG_FALSE;
    readpnm(pnm_nr, &ptpos, ptend, width, height, maxval, filename, imgp);
    if (ptpos == NULL) { vg4->image->destroy(imgp); return NULL; }

  } else {
    imgp = vg4data.iolib.f.image_load(filename, ptpos, (size_t)(ptend - ptpos));
  }

  return imgp;
} /* Ende vg4_image_loadmem */


/* vg4_image_savefile:
 * save image to file according to format
 * @param imgp      image
 * @param filename  filename to save
 * @param noalpha   if VG_TRUE, set alpha to 255
 * @return  VG_TRUE = OK or VG_FALSE = error
 */
VG_BOOL
vg4_image_savefile(const struct VG_Image *imgp, const char *filename, VG_BOOL noalpha)
{
  enum { SAVE_AS_BMP = 1, SAVE_AS_PPM, SAVE_AS_PGM, SAVE_AS_PBM, SAVE_AS_PAM } save_as;
  const char *extptr;
  FILE *ffp;
  int w, h;
  struct VG_PixelColor *pixptr;

  if (imgp == NULL) { outerr("saving image: no image to save"); return VG_FALSE; }
  if (filename == NULL || *filename == '\0') { outerr("saving image: no filename"); return VG_FALSE; }

  save_as = SAVE_AS_BMP;
  if ((extptr = strrchr(filename, '.')) != NULL) {
    if (strcasecmp(extptr, ".ppm") == 0) {
      save_as = SAVE_AS_PPM;
    } else if (strcasecmp(extptr, ".pgm") == 0) {
      save_as = SAVE_AS_PGM;
    } else if (strcasecmp(extptr, ".pbm") == 0) {
      save_as = SAVE_AS_PBM;
    } else if (strcasecmp(extptr, ".pam") == 0) {
      save_as = SAVE_AS_PAM;
    }
  }

  if (save_as == SAVE_AS_BMP) {
    return vg4data.iolib.f.image_save(imgp, filename, noalpha);
  }

  if ((ffp = fopen(filename, "w")) == NULL) {
    outerr("saving image \"%s\": %s", filename, strerror(errno));
    return VG_FALSE;
  }

  if (save_as == SAVE_AS_PPM) {
    fprintf(ffp, "P6\n%d\n%d\n255\n", imgp->img.w, imgp->img.h);
    for (h = 0; h < imgp->img.h; h++) {
      for (w = 0; w < imgp->img.w; w++) {
        pixptr = &imgp->img.pixels[h * imgp->img.w + w];
        fwrite(&pixptr->r, 1, 1, ffp);
        fwrite(&pixptr->g, 1, 1, ffp);
        if (fwrite(&pixptr->b, 1, 1, ffp) != 1) {
          outerr("saving image \"%s\": short write", filename);
          fclose(ffp);
          return VG_FALSE;
        }
      }
    }

  } else if (save_as == SAVE_AS_PGM) {
    unsigned char grey;
    fprintf(ffp, "P5\n%d\n%d\n255\n", imgp->img.w, imgp->img.h);
    for (h = 0; h < imgp->img.h; h++) {
      for (w = 0; w < imgp->img.w; w++) {
        pixptr = &imgp->img.pixels[h * imgp->img.w + w];
        grey = ((unsigned int)pixptr->r + (unsigned int)pixptr->g + (unsigned int)pixptr->b) / 3;
        if (fwrite(&grey, 1, 1, ffp) != 1) {
          outerr("saving image \"%s\": short write", filename);
          fclose(ffp);
          return VG_FALSE;
        }
      }
    }

  } else if (save_as == SAVE_AS_PBM) {
    unsigned char bitval;
    int bitpos;
    fprintf(ffp, "P4\n%d\n%d\n", imgp->img.w, imgp->img.h);
    for (h = 0; h < imgp->img.h; h++) {
      bitpos = 7;
      bitval = 0;
      for (w = 0; w < imgp->img.w; w++) {
        pixptr = &imgp->img.pixels[h * imgp->img.w + w];
        if (!pixptr->r && !pixptr->g && !pixptr->b) { bitval |= (1 << bitpos); }
        if (--bitpos < 0) {
          if (fwrite(&bitval, 1, 1, ffp) != 1) {
            outerr("saving image \"%s\": short write", filename);
            fclose(ffp);
            return VG_FALSE;
          }
          bitpos = 7;
          bitval = 0;
        }
      }
      if (bitpos < 7) {
        if (fwrite(&bitval, 1, 1, ffp) != 1) {
          outerr("saving image \"%s\": short write", filename);
          fclose(ffp);
          return VG_FALSE;
        }
      }
    }

  } else if (save_as == SAVE_AS_PAM) {
    size_t written;
    fprintf(ffp, "P7\nWIDTH %d\nHEIGHT %d\nDEPTH 4\nMAXVAL 255\nTUPLTYPE %s\nENDHDR\n",
            imgp->img.w, imgp->img.h, (noalpha ? "RGB" : "RGB_ALPHA"));
    for (h = 0; h < imgp->img.h; h++) {
      for (w = 0; w < imgp->img.w; w++) {
        pixptr = &imgp->img.pixels[h * imgp->img.w + w];
        fwrite(&pixptr->r, 1, 1, ffp);
        fwrite(&pixptr->g, 1, 1, ffp);
        written = fwrite(&pixptr->b, 1, 1, ffp);
        if (!noalpha) { written = fwrite(&pixptr->a, 1, 1, ffp); }
        if (written != 1) {
          outerr("saving image \"%s\": short write", filename);
          fclose(ffp);
          return VG_FALSE;
        }
      }
    }
  }

  fclose(ffp);

  return VG_TRUE;
} /* Ende vg4_image_savefile */


/* vg4_image_tobase64:
 * convert image to base64 (e.g. for text control-command "img")
 * @param imgp      image
 * @param noalpha   if VG_TRUE, set alpha to 255
 * @param encdata   for returning encoded data (with terminating 0), (must be freed with free())
 * @param encsize   for returning number of bytes in encdata (without terminating 0)
 * @return  VG_TRUE = OK or VG_FALSE = error
 */
VG_BOOL
vg4_image_tobase64(const struct VG_Image *imgp, VG_BOOL noalpha, char **encdata, size_t *encsize)
{
  int w, h;
  struct VG_PixelColor *pixptr;
  struct SML3_gummi gm1 = SML3_GUM_INITIALIZER;
  int gmpos;

  if (imgp == NULL || encdata == NULL || encsize == NULL) { outerr("image to base64: parameter error"); return VG_FALSE; }

  gmpos = SML3_gumprintf(&gm1, 0, "P7\nWIDTH %d\nHEIGHT %d\nDEPTH 4\nMAXVAL 255\nTUPLTYPE %s\nENDHDR\n",
                         imgp->img.w, imgp->img.h, (noalpha ? "RGB" : "RGB_ALPHA"));

  for (h = 0; h < imgp->img.h; h++) {
    for (w = 0; w < imgp->img.w; w++) {
      pixptr = &imgp->img.pixels[h * imgp->img.w + w];
      SML3_gumset(&gm1, gmpos, pixptr->r, 1); gmpos++;
      SML3_gumset(&gm1, gmpos, pixptr->g, 1); gmpos++;
      SML3_gumset(&gm1, gmpos, pixptr->b, 1); gmpos++;
      if (!noalpha) { SML3_gumset(&gm1, gmpos, pixptr->a, 1); gmpos++; }
    }
  }

  SML3_base64_encode(encdata, encsize, SML3_gumgetval(&gm1), gmpos, 0, NULL);
  SML3_gumdest(&gm1);

  return VG_TRUE;
} /* Ende vg4_image_tobase64 */
