/********************************************************************************

   Fotoxx      edit photos and manage collections

   Copyright 2007-2020 Michael Cornelison
   source code URL: https://kornelix.net
   contact: mkornelix@gmail.com

   This program 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 3 of the License, or
   (at your option) any later version. See https://www.gnu.org/licenses

   This program 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.

*********************************************************************************

   Fotoxx image edit - functions for pixmap memory images

   PXM_make                create PXM pixmap  RGB[A] float
   PXM_free                free PXM pixmap memory
   PXM_audit               audit PXM pixmap for errors
   PXM_clear               clear PXM pixmap white/opaque or black/transparent
   PXM_addalpha            add alhpa channel to PXM pixmap
   PXM_subalpha            remove alhpa channel from PXM pixmap
   PXM_copy                copy (duplicate) PXM pixmap
   PXM_copy_area           copy area from one PXM pixmap to another
   PXM_rescale             copy and rescale PXM pixmap
   PXM_rotate              copy and rotate PXM pixmap
   
   PXB_make                create PXB pixmap  RGB[A] uint8
   PXB_free                free PXB pixmap memory
   PXB_addalpha            add alhpa channel to PXB pixmap
   PXB_subalpha            remove alpha channel from PXB pixmap
   PXB_copy                copy (duplicate) PXB pixmap
   PXB_copy_area           copy area from one PXB into another
   PXB_subpxb              create new PXB from section of existing PXB
   PXB_half                rescale PXB pixmap to 1/2 size
   PXB_rescale             rescale PXB pixmap - ww/hh independent
   PXB_resize              resize PXB pixmap - max. ww/hh, preserve ratio
   PXB_rescale_fast        fast rescale PXB pixmap - ww/hh independent
   PXB_resize_fast         fast resize PXB pixmap - max. ww/hh, preserve ratio
   PXB_rotate              rotate PXB pixmap, any angle

   vpixel                  get virtual pixel at any PXM or PXB image location (float)
   
   PXM_PXB_copy            copy PXM pixmap to PXB pixmap
   PXM_PXB_update          copy/convert PXM pixmap area to PXB pixmap area
   PXB_PXB_update          copy/rescale input PXB pixmap area to output PXB area
   
   PXB_load                load an image file into a PXB pixmap (8-bit RGB)
   PXM_load                load an image file into a PXM pixmap (float RGB)
   PXB_save                save a PXB pixmap to a file with (8 bit RGB)
   PXM_save                save a PXM pixmap to a file with (8/16 bit RGB)
   
   JPG_PXB_load            load a .jpg file into a PXB pixmap (8-bit RGB)
   JPG_PXM_load            load a .jpg file into a PXM pixmap (float RGB)
   PXB_JPG_save            save a PXB pixmap to a .jpg file (8-bit RGB)
   PXM_JPG_save            save a PXM pixmap to a .jpg file (8-bit RGB)

   TIFF_PXB_load           load a .tif file into a PXB pixmap (8-bit RGB)
   TIFF_PXM_load           load a .tif file into a PXM pixmap (float RGB)
   PXB_TIFF_save           save a PXB pixmap to a .tif file (8/16-bit RGB)
   PXM_TIFF_save           save a PXM pixmap to a .tif file (8/16-bit RGB)

   PNG_PXB_load            load a .png file into a PXB pixmap (8-bit RGB)
   PNG_PXM_load            load a .png file into a PXM pixmap (float RGB)
   PXB_PNG_save            save a PXB pixmap to a .png file (8/16-bit RGB)
   PXM_PNG_save            save a PXM pixmap to a .png file (8/16-bit RGB)

   HEIC_PXB_load            load a .heic file into a PXB pixmap (8-bit RGB)      20.0
   HEIC_PXM_load            load a .heic file into a PXM pixmap (float RGB)
   PXB_HEIC_save            save a PXB pixmap to a .heic file (8/16-bit RGB)
   PXM_HEIC_save            save a PXM pixmap to a .heic file (8/16-bit RGB)

   JP2_PXB_load            load a .jp2 file into a PXB pixmap (8-bit RGB)        20.0
   JP2_PXM_load            load a .jp2 file into a PXM pixmap (float RGB)
   PXB_JP2_save            save a PXB pixmap to a .jp2 file (8/16-bit RGB)
   PXM_JP2_save            save a PXM pixmap to a .jp2 file (8/16-bit RGB)

   ANY_PXB_load            load other image file into a PXB pixmap (8-bit RGB)
   ANY_PXM_load            load other image file into a PXM pixmap (float RGB)

   RAW_PXB_load            load a RAW file into a PXB pixmap (8-bit RGB)
   RAW_PXM_load            load a RAW file into a PXM pixmap (float RGB)
   
   RAW_PXB_load_dcraw      load a RAW file into a PXB pixmap (8-bit RGB)         20.0
   RAW_PXM_load_dcraw      load a RAW file into a PXM pixmap (float RGB)

   RAW_PXB_load_rawtherapee   load a RAW file into a PXB pixmap (8-bit RGB)      20.0
   RAW_PXM_load_rawtherapee   load a RAW file into a PXM pixmap (float RGB)
   
   pixelvert               convert pixel format and channel count

*********************************************************************************/

#define EX extern                                                                //  disable extern declarations
#include "fotoxx.h"                                                              //  (variables in fotoxx.h are refs)


/*********************************************************************************
      PXM pixmap functions - RGB[A] float pixel map
      pixel RGB values may range from 0.0 to 255.99
*********************************************************************************/

//  initialize PXM pixmap - allocate memory

PXM * PXM_make(int ww, int hh, int nc)
{
   int64    ww6 = ww, hh6 = hh;
   int64    npix = ww6 * hh6;
   int64    cc = npix * nc * sizeof(float);

   if (ww < 10 || hh < 10) {                                                     //  impose reasonableness limits
      zmessageACK(Mwin,"image too small: %d x %d",ww,hh);
      return 0;
   }

   if (ww > wwhh_limit1 || hh > wwhh_limit1 || npix > wwhh_limit2) {             //  avoid image dimensions too big
      zmessageACK(Mwin,"image too big: %dx%d",ww,hh);
      return 0;
   }
   
   PXM *pxm = (PXM *) zmalloc(sizeof(PXM));
   strcpy(pxm->wmi,"pxmpix");
   pxm->ww = ww;
   pxm->hh = hh;
   pxm->nc = nc;                                                                 //  nc = 3/4 for no/alpha channel
   pxm->rs = ww * nc;                                                            //  bytes = rs * sizeof(float)         19.0

   pxm->pixels = (float *) zmalloc(cc);                                          //  crashes if too big
   return pxm;
}


//  free PXM pixmap

void PXM_free(PXM *&pxm)
{
   if (! pxm) return;
   if (! strmatch(pxm->wmi,"pxmpix"))
      zappcrash("PXM_free(), bad PXM %s",pxm->wmi);
   strcpy(pxm->wmi,"xxxxxx");
   zfree(pxm->pixels);
   zfree(pxm);
   pxm = 0;
   return;
}


//  audit the contents of a PXM pixmap

void PXM_audit(PXM *pxm)
{
   int      px, py;
   int      err1 = 0, err2 = 0;                                                  //  check RGB within bounds            20.0
   float    *pix;

   for (py = 0; py < pxm->hh; py++)
   for (px = 0; px < pxm->ww; px++)
   {
      pix = PXMpix(pxm,px,py);

      for (int ii = 0; ii < 3; ii++) 
      {
         if (pix[ii] < 0) {
            err1 = 1;
            pix[ii] = 0;
         }
         
         if (pix[ii] >= 256.0) {
            err2 = 1;
            pix[ii] = 255.9;
         }
      }
   }
   
   if (err1) printz("PXM_audit(): corrected RGB < 0 \n");
   if (err2) printz("PXM_audit(): corrected RGB >= 256.0 \n");

   return;
}


//  clear a PXM pixmap to white/opaque or black/transparent

void PXM_clear(PXM *pxm, int BW)
{
   int      px, py, ii;
   float    *pix, rgba;

   if (BW) rgba = 255.0;
   else rgba = 0;

   for (py = 0; py < pxm->hh; py++)
   for (px = 0; px < pxm->ww; px++)
   {
      pix = PXMpix(pxm,px,py);

      for (ii = 0; ii < pxm->nc; ii++) 
         pix[ii] = rgba;
   }
   
   return;
}


//  add an alpha channel to a PXM pixmap (RGB >> RGBA)

void PXM_addalpha(PXM *pxm)
{
   int64    cc;                                                                  //  19.15
   int      ww, hh, nc1, nc2, ii;
   float    *pixels1, *pixels2, *pix1, *pix2;
   
   ww = pxm->ww;
   hh = pxm->hh;
   nc1 = pxm->nc;
   if (nc1 > 3) return;
   pixels1 = pxm->pixels;
   
   nc2 = nc1 + 1;
   cc = ww * hh * nc2 * sizeof(float);
   pixels2 = (float *) zmalloc(cc);

   cc = nc1 * sizeof(float);
   pix1 = pixels1;
   pix2 = pixels2;

   for (ii = 0; ii < ww * hh; ii++) {
      memcpy(pix2,pix1,cc);
      pix2[nc1] = 255.0;                                                         //  100% opaque = 255
      pix1 += nc1;
      pix2 += nc2;
   }
   
   zfree(pixels1);
   pxm->nc = nc2;
   pxm->rs = ww * nc2;                                                           //  bytes = rs * sizeof(float)         19.0
   pxm->pixels = pixels2;

   return;
}


//  remove alpha channel from a PXM pixmap (RGBA >> RGB)

void PXM_subalpha(PXM *pxm)                                                      //  20.0
{
   uint     cc;
   int      ww, hh, nc1, nc2, ii;
   float    *pixels1, *pixels2, *pix1, *pix2;
   
   ww = pxm->ww;
   hh = pxm->hh;
   nc1 = pxm->nc;
   if (nc1 < 4) return;
   pixels1 = pxm->pixels;

   nc2 = 3;
   cc = ww * hh * nc2 * sizeof(float);
   pixels2 = (float *) zmalloc(cc);

   cc = nc2 * sizeof(float);
   pix1 = pixels1;
   pix2 = pixels2;

   for (ii = 0; ii < ww * hh; ii++) {
      memcpy(pix2,pix1,cc);
      pix1 += nc1;
      pix2 += nc2;
   }
   
   zfree(pixels1);
   pxm->nc = nc2;
   pxm->rs = ww * nc2;
   pxm->pixels = pixels2;
   return;
}


//  create a copy of a PXM pixmap

PXM * PXM_copy(PXM *pxm1)
{
   uint     cc;
   int      ww, hh, nc;
   PXM      *pxm2;
   
   ww = pxm1->ww;
   hh = pxm1->hh;
   nc = pxm1->nc;
   cc = ww * hh * nc * sizeof(float);

   pxm2 = PXM_make(ww,hh,nc);
   if (! pxm2) return 0;

   memcpy(pxm2->pixels,pxm1->pixels,cc);
   return pxm2;
}


//  create a copy of a PXM pixmap rectangular area

PXM * PXM_copy_area(PXM *pxm1, int orgx, int orgy, int ww2, int hh2)
{
   float    *pix1, *pix2;
   PXM      *pxm2 = 0;
   int      px1, py1, px2, py2;
   int      nc, pcc;
   
   nc = pxm1->nc;
   pcc = nc * sizeof(float);

   pxm2 = PXM_make(ww2,hh2,nc);
   if (! pxm2) return 0;

   for (py1 = orgy, py2 = 0; py2 < hh2; py1++, py2++)
   {
      pix1 = PXMpix(pxm1,orgx,orgy);
      pix2 = PXMpix(pxm2,0,py2);

      for (px1 = orgx, px2 = 0; px2 < ww2; px1++, px2++)
      {
         memcpy(pix2,pix1,pcc);
         pix1 += nc;
         pix2 += nc;
      }
   }

   return pxm2;
}


/********************************************************************************

   Rescale PXM pixmap to new width and height.
   The scale ratios may be different for width and height.

   Method is better than bilinear:
      For all input pixels [p] having some overlap area with an output pixel:
         output pixel = sum ( overlap[p] * input[p] ) / sum ( overlap[p] )

   NOT THREAD SAFE

*********************************************************************************/

namespace pxmrescale {
   float    *ppix1, *ppix2;
   int      ww1, hh1, ww2, hh2, nc;
   int      *px1L, *py1L;
   float    *pxmap, *pymap;
   int      maxmapx, maxmapy;
   int      nwt;
}


PXM * PXM_rescale(PXM *pxm1, int ww, int hh)
{
   using namespace pxmrescale;

   void * pxm_rescale_thread(void *arg);

   PXM         *pxm2;
   int         px1, py1, px2, py2;
   int         pxl, pyl, pxm, pym, ii;
   float       scalex, scaley;
   float       px1a, py1a, px1b, py1b;
   float       fx, fy;

   ww1 = pxm1->ww;                                                               //  input PXM
   hh1 = pxm1->hh;
   nc = pxm1->nc; 
   ppix1 = pxm1->pixels;

   pxm2 = PXM_make(ww,hh,nc);                                                    //  output PXM
   if (! pxm2) return 0;                                                         //  too big or no memory
   ww2 = ww;
   hh2 = hh;
   ppix2 = pxm2->pixels;

   memset(ppix2, 0, ww2 * hh2 * nc * sizeof(float));                             //  clear output pixmap

   scalex = 1.0 * ww1 / ww2;                                                     //  compute x and y scales
   scaley = 1.0 * hh1 / hh2;

   if (scalex <= 1) maxmapx = 2;                                                 //  compute max input pixels
   else maxmapx = scalex + 2;                                                    //    mapping into output pixels
   maxmapx += 1;                                                                 //      for both dimensions
   if (scaley <= 1) maxmapy = 2;                                                 //  (pixels may not be square)
   else maxmapy = scaley + 2;
   maxmapy += 1;                                                                 //  (extra entry for -1 flag)

   pymap = (float *) zmalloc(hh2 * maxmapy * sizeof(float));                     //  maps overlap of < maxmap input
   pxmap = (float *) zmalloc(ww2 * maxmapx * sizeof(float));                     //    pixels per output pixel

   py1L = (int *) zmalloc(hh2 * sizeof(int));                                    //  maps first (lowest) input pixel
   px1L = (int *) zmalloc(ww2 * sizeof(int));                                    //    per output pixel

   for (py2 = 0; py2 < hh2; py2++)                                               //  loop output y-pixels
   {
      py1a = py2 * scaley;                                                       //  corresponding input y-pixels
      py1b = py1a + scaley;
      if (py1b >= hh1) py1b = hh1 - 0.001;                                       //  fix precision limitation
      pyl = py1a;
      py1L[py2] = pyl;                                                           //  1st overlapping input pixel

      for (py1 = pyl, pym = 0; py1 < py1b; py1++, pym++)                         //  loop overlapping input pixels
      {
         if (py1 < py1a) {                                                       //  compute amount of overlap
            if (py1+1 < py1b) fy = py1+1 - py1a;                                 //    0.0 to 1.0
            else fy = scaley;
         }
         else if (py1+1 > py1b) fy = py1b - py1;
         else fy = 1;

         ii = py2 * maxmapy + pym;                                               //  save it
         pymap[ii] = 0.9999 * fy / scaley;
      }
      ii = py2 * maxmapy + pym;                                                  //  set an end marker after
      pymap[ii] = -1;                                                            //    last overlapping pixel
   }

   for (px2 = 0; px2 < ww2; px2++)                                               //  do same for x-pixels
   {
      px1a = px2 * scalex;
      px1b = px1a + scalex;
      if (px1b >= ww1) px1b = ww1 - 0.001;
      pxl = px1a;
      px1L[px2] = pxl;

      for (px1 = pxl, pxm = 0; px1 < px1b; px1++, pxm++)
      {
         if (px1 < px1a) {
            if (px1+1 < px1b) fx = px1+1 - px1a;
            else fx = scalex;
         }
         else if (px1+1 > px1b) fx = px1b - px1;
         else fx = 1;

         ii = px2 * maxmapx + pxm;
         pxmap[ii] = 0.9999 * fx / scalex;
      }
      ii = px2 * maxmapx + pxm;
      pxmap[ii] = -1;
   }
   
   nwt = NWT;                                                                    //  threads
   if (nwt > 4) nwt = 4;                                                         //  slower > 4                         19.15
   do_wthreads(pxm_rescale_thread,nwt);

   zfree(px1L);
   zfree(py1L);
   zfree(pxmap);
   zfree(pymap);

   return pxm2;
}


void * pxm_rescale_thread(void *arg)                                             //  worker thread function
{
   using namespace pxmrescale;

   int         index = *((int *) arg);
   int         px1, py1, px2, py2;
   int         pxl, pyl, pxm, pym, ii;
   float       *pixel1, *pixel2;
   float       fx, fy, ftot;
   float       chan[6];
   int         pcc = nc * sizeof(float);

   for (py2 = index; py2 < hh2; py2 += nwt)                                      //  loop output y-pixels
   {
      pyl = py1L[py2];                                                           //  corresp. 1st input y-pixel

      for (px2 = 0; px2 < ww2; px2++)                                            //  loop output x-pixels
      {
         pxl = px1L[px2];                                                        //  corresp. 1st input x-pixel

         memset(chan,0,pcc);                                                     //  initz. output pixel

         for (py1 = pyl, pym = 0; ; py1++, pym++)                                //  loop overlapping input y-pixels
         {
            ii = py2 * maxmapy + pym;                                            //  get y-overlap
            fy = pymap[ii];
            if (fy < 0) break;                                                   //  no more pixels

            for (px1 = pxl, pxm = 0; ; px1++, pxm++)                             //  loop overlapping input x-pixels
            {
               ii = px2 * maxmapx + pxm;                                         //  get x-overlap
               fx = pxmap[ii];
               if (fx < 0) break;                                                //  no more pixels

               ftot = fx * fy;                                                   //  area overlap = x * y overlap
               pixel1 = ppix1 + (py1 * ww1 + px1) * nc;
               for (ii = 0; ii < nc; ii++)
                  chan[ii] += pixel1[ii] * ftot;                                 //  add input pixel * overlap
            }
         }

         pixel2 = ppix2 + (py2 * ww2 + px2) * nc;                                //  save output pixel
         memcpy(pixel2,chan,pcc);
      }
   }

   pthread_exit(0);
}


/********************************************************************************

   PXM *pxm2 = PXM_rotate(PXM *pxm1, float angle)

   Rotate PXM pixmap through an arbitrary angle (degrees).

   The returned image has the same size as the original, but the
   pixmap size is increased to accommodate the rotated image.
   (e.g. a 100x100 image rotated 45 deg. needs a 142x142 pixmap).

   The space added around the rotated image is black (RGB 0,0,0).
   Angle is in degrees. Positive direction is clockwise.
   Speed is about 28 million pixels/sec/thread for a 3.3 GHz CPU.
   Loss of resolution is less than 1 pixel.

   NOT THREAD SAFE

*********************************************************************************/

namespace pxmrotate {
   float    *ppix1, *ppix2;
   int      ww1, hh1, ww2, hh2, nc;
   float    angle;
}


PXM * PXM_rotate(PXM *pxm1, float anglex, int fast)                              //  fast option
{
   using namespace pxmrotate;

   PXM *PXM_rotate90(PXM *pxm, int angle);
   void *PXM_rotate_thread(void *);
   void *PXM_rotate_thread_fast(void *);

   PXM      *pxm2;

   ww1 = pxm1->ww;                                                               //  input PXM
   hh1 = pxm1->hh;
   nc = pxm1->nc;
   ppix1 = pxm1->pixels;
   angle = anglex;

   while (angle < -180) angle += 360;                                            //  normalize, -180 to +180
   while (angle > 180) angle -= 360;

   if (angle >= -180.0 && angle < -179.99)                                       //  use lossless version for angles
      return PXM_rotate90(pxm1,180);                                             //    of -180, -90, 0, 90, 180
   if (angle > -90.01 && angle < -89.99)
      return PXM_rotate90(pxm1,-90);
   if (angle > -0.01 && angle < 0.01)
      return PXM_copy(pxm1);
   if (angle > 89.99 && angle < 90.01)
      return PXM_rotate90(pxm1,90);
   if (angle > 179.99 && angle <= 180.0)
      return PXM_rotate90(pxm1,180);

   angle = angle * PI / 180;                                                     //  radians, -PI to +PI

   ww2 = ww1*fabsf(cosf(angle)) + hh1*fabsf(sinf(angle));                        //  rectangle containing rotated image
   hh2 = ww1*fabsf(sinf(angle)) + hh1*fabsf(cosf(angle));

   pxm2 = PXM_make(ww2,hh2,nc);                                                  //  output PXM
   if (! pxm2) return 0;
   ppix2 = pxm2->pixels;
   
   if (fast) do_wthreads(PXM_rotate_thread_fast,NWT);
   else do_wthreads(PXM_rotate_thread,NWT);

   return pxm2;
}


void * PXM_rotate_thread(void *arg)
{
   using namespace pxmrotate;

   int      index = *((int *) (arg));
   int      px2, py2, px0, py0;
   float    *pix0, *pix1, *pix2, *pix3;
   float    px1, py1;
   float    f0, f1, f2, f3, chan[6];
   float    a, b, ww15, hh15, ww25, hh25;
   int      pcc = nc * sizeof(float);

   ww15 = 0.5 * ww1;
   hh15 = 0.5 * hh1;
   ww25 = 0.5 * ww2;
   hh25 = 0.5 * hh2;

   a = cosf(angle);
   b = sinf(angle);

   for (py2 = index; py2 < hh2; py2 += NWT)                                      //  loop through output pixels
   for (px2 = 0; px2 < ww2; px2++)
   {
      px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15;                          //  (px1,py1) = corresponding
      py1 = -b * (px2 - ww25) + a * (py2 - hh25) + hh15;                         //    point within input pixels

      px0 = px1;                                                                 //  pixel containing (px1,py1)
      py0 = py1;

      if (px0 < 0 || px0 > ww1-2 || py0 < 0 || py0 > hh1-2) {                    //  if outside input pixel array
         pix2 = ppix2 + (py2 * ww2 + px2) * nc;                                  //    output is black
         memset(pix2,0,pcc);
         continue;
      }

      pix0 = ppix1 + (py0 * ww1 + px0) * nc;                                     //  4 input pixels based at (px0,py0)
      pix1 = pix0 + ww1 * nc;
      pix2 = pix0 + nc;
      pix3 = pix1 + nc;

      f0 = (px0+1 - px1) * (py0+1 - py1);                                        //  overlap of (px1,py1)
      f1 = (px0+1 - px1) * (py1 - py0);                                          //    in each of the 4 pixels
      f2 = (px1 - px0) * (py0+1 - py1);
      f3 = (px1 - px0) * (py1 - py0);
      
      for (int ii = 0; ii < nc; ii++)                                            //  sum the weighted inputs
         chan[ii] = f0 * pix0[ii] + f1 * pix1[ii] 
                  + f2 * pix2[ii] + f3 * pix3[ii];

      pix2 = ppix2 + (py2 * ww2 + px2) * nc;                                     //  output pixel
      memcpy(pix2,chan,pcc);
   }

   pthread_exit(0);
}


void * PXM_rotate_thread_fast(void *arg)
{
   using namespace pxmrotate;

   int      index = *((int *) (arg));
   int      px2, py2, px0, py0;
   float    *pix0, *pix2;
   float    px1, py1;
   float    a, b, ww15, hh15, ww25, hh25;
   int      pcc = nc * sizeof(float);

   ww15 = 0.5 * ww1;
   hh15 = 0.5 * hh1;
   ww25 = 0.5 * ww2;
   hh25 = 0.5 * hh2;

   a = cosf(angle);
   b = sinf(angle);

   for (py2 = index; py2 < hh2; py2 += NWT)                                      //  loop through output pixels
   for (px2 = 0; px2 < ww2; px2++)
   {
      px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15;                          //  (px1,py1) = corresponding
      py1 = -b * (px2 - ww25) + a * (py2 - hh25) + hh15;                         //    point within input pixels

      px0 = px1;                                                                 //  pixel containing (px1,py1)
      py0 = py1;

      if (px0 < 0 || px0 > ww1-2 || py0 < 0 || py0 > hh1-2) {                    //  if outside input pixel array
         pix2 = ppix2 + (py2 * ww2 + px2) * nc;                                  //    output is black
         memset(pix2,0,pcc);
         continue;
      }

      pix0 = ppix1 + (py0 * ww1 + px0) * nc;                                     //  input pixel
      pix2 = ppix2 + (py2 * ww2 + px2) * nc;                                     //  output pixel
      memcpy(pix2,pix0,pcc);
   }

   pthread_exit(0);
}


PXM * PXM_rotate90(PXM *pxm1, int angle)                                          //  angle = -90, 90, 180
{
   using namespace pxmrotate;

   int      px1, py1, px2, py2, nc, pcc;
   float    *pix1, *pix2;
   PXM      *pxm2;

   if (angle == 0) return PXM_copy(pxm1);

   ww1 = pxm1->ww;                                                               //  input PXM
   hh1 = pxm1->hh;
   nc = pxm1->nc;
   pcc = nc * sizeof(float);

   if (angle == -90) {
      ww2 = hh1;
      hh2 = ww1;
   }

   else if (angle == 90) {
      ww2 = hh1;
      hh2 = ww1;
   }

   else if (angle == 180) {
      ww2 = ww1;
      hh2 = hh1;
   }

   else zappcrash("PXM_rotate2() bad angle %d",angle);

   pxm2 = PXM_make(ww2,hh2,nc);                                                  //  output PXM
   if (! pxm2) return 0;

   ppix2 = pxm2->pixels;

   for (py1 = 0; py1 < hh1; py1++)                                               //  loop all input pixels
   for (px1 = 0; px1 < ww1; px1++)
   {
      if (angle == -90) {
         px2 = py1;
         py2 = hh2 - px1 - 1;
      }

      else if (angle == 90) {
         px2 = ww2 - py1 - 1;
         py2 = px1;
      }

      else /* angle = 180 */ {
         px2 = ww2 - px1 - 1;
         py2 = hh2 - py1 - 1;
      }

      pix1 = ppix1 + (py1 * ww1 + px1) * nc;
      pix2 = ppix2 + (py2 * ww2 + px2) * nc;
      memcpy(pix2,pix1,pcc);
   }

   return pxm2;
}


/********************************************************************************
      PXB pixmap functions - RGB[A] uint8 pixel map and PIXBUF wrapper
*********************************************************************************/

//  Create PXB pixmap with pixels cleared to zero
//  nc = 3/4 for RGB/RGBA image

PXB * PXB_make(int ww, int hh, int nc)                                           //  nc instead of ac                   19.0
{
   uint     rs, cc, ac;
   uint8    *pixels;

   if (nc != 3 && nc != 4) zappcrash("PXB_make() nc: %d",nc);
   PXB *pxb = (PXB *) zmalloc(sizeof(PXB));
   strcpy(pxb->wmi,"pxbpix");
   pxb->ww = ww;
   pxb->hh = hh;
   pxb->nc = nc;
   rs = ww * nc;
   pxb->rs = rs;

   cc = ww * hh * nc;
   pixels = (uint8 *) zmalloc(cc);
   pxb->pixels = pixels;

   ac = nc - 3;                                                                  //  alpha channel 0/1
   pxb->pixbuf = gdk_pixbuf_new_from_data(pixels,GDKRGB,ac,8,ww,hh,rs,0,0);      //  make congruent pixbuf
   if (! pxb->pixbuf) {
      zmessageACK(Mwin,"PXB_make(): pixbuf allocation failure");
      quitxx();
   }

   return pxb;
}


//  Free PXB pixmap - release memory

void PXB_free(PXB *&pxb)
{
   if (! pxb) return;
   if (! strmatch(pxb->wmi,"pxbpix"))
      zappcrash("PXB_free(), bad PXB");
   strcpy(pxb->wmi,"xxxxxx");
   g_object_unref(pxb->pixbuf);
   zfree(pxb->pixels);
   zfree(pxb);
   pxb = 0;
   return;
}


//  add an alpha channel to a PXB pixmap

void PXB_addalpha(PXB *pxb)
{
   uint8       *pixels1, *pixels2, *pix1, *pix2;
   int         ww, hh, nc, rs, cc;

   nc = pxb->nc;
   if (nc == 4) return;
   if (nc != 3) zappcrash("PXB_addalpha() nc: %d",nc);
   
   ww = pxb->ww;
   hh = pxb->hh;
   pixels1 = pxb->pixels;
   cc = ww * hh * 4;
   pixels2 = (uint8 *) zmalloc(cc);
   
   pix1 = pixels1;
   pix2 = pixels2;
   
   for (int ii = 0; ii < ww * hh; ii++)
   {
      memcpy(pix2,pix1,3);
      pix2[3] = 255;                                                             //  opacity = max.
      pix1 += 3;
      pix2 += 4;
   }
   
   zfree(pixels1);
   pxb->pixels = pixels2;
   pxb->nc = 4;
   rs = ww * 4;
   pxb->rs = rs;

   g_object_unref(pxb->pixbuf);
   pxb->pixbuf = gdk_pixbuf_new_from_data(pixels2,GDKRGB,1,8,ww,hh,rs,0,0);
   if (! pxb->pixbuf) {
      zmessageACK(Mwin,"PXB_addalpha(): pixbuf allocation failure");
      quitxx();
   }

   return;
}


//  remove an alpha channel from a PXB pixmap

void PXB_subalpha(PXB *pxb)
{
   uint8       *pixels1, *pixels2, *pix1, *pix2;
   int         ww, hh, nc, rs, cc;

   nc = pxb->nc;
   if (nc == 3) return;
   if (nc != 4) zappcrash("PXB_subalpha() nc: %d",nc);
   
   ww = pxb->ww;
   hh = pxb->hh;
   pixels1 = pxb->pixels;
   cc = ww * hh * 3;
   pixels2 = (uint8 *) zmalloc(cc);
   
   pix1 = pixels1;
   pix2 = pixels2;
   
   for (int ii = 0; ii < ww * hh; ii++)
   {
      memcpy(pix2,pix1,3);
      pix1 += 4;
      pix2 += 3;
   }
   
   zfree(pixels1);
   pxb->pixels = pixels2;
   pxb->nc = 3;
   rs = ww * 3;
   pxb->rs = rs;

   g_object_unref(pxb->pixbuf);
   pxb->pixbuf = gdk_pixbuf_new_from_data(pixels2,GDKRGB,0,8,ww,hh,rs,0,0);
   if (! pxb->pixbuf) {
      zmessageACK(Mwin,"PXB_subalpha(): pixbuf allocation failure");
      quitxx();
   }

   return;
}


//  Copy a PXB pixmap to a new PXB pixmap

PXB * PXB_copy(PXB *pxb1)
{
   int      ww, hh, nc, cc;

   ww = pxb1->ww;
   hh = pxb1->hh;
   nc = pxb1->nc;
   PXB *pxb2 = PXB_make(ww,hh,nc);
   cc = ww * hh * nc;
   memcpy(pxb2->pixels,pxb1->pixels,cc);
   return pxb2;
}


//  copy an area from an input PXB to an output PXB
//  input area (px1, py1, ww, hh) copied to output area at (px2, py2)

void PXB_copy_area(PXB *pxb1, int px1, int py1, int ww, int hh, PXB *pxb2, int px2, int py2)
{
   int      row1, row2;

   if (px1 < 0) goto overflow;
   if (px1 + ww > pxb1->ww) goto overflow;
   if (py1 < 0) goto overflow;
   if (py1 + hh > pxb1->hh) goto overflow;

   if (px2 < 0) goto overflow;
   if (px2 + ww > pxb2->ww) goto overflow;
   if (py2 < 0) goto overflow;
   if (py2 + hh > pxb2->hh) goto overflow;
   
   if (pxb1->nc != pxb2->nc) zappcrash("PXB_copy_area(): NC unequal");

   for (row1 = py1, row2 = py2; row1 < py1 + hh; row1++, row2++)
      memcpy(pxb2->pixels + row2 * pxb2->rs + px2 * pxb2->nc, 
             pxb1->pixels + row1 * pxb1->rs + px1 * pxb1->nc, ww * pxb1->nc);
   return;

overflow:
   zappcrash("PXB_copy_area() overflow:  input: %d %d %d %d  "
             "output: %d %d",px1,py1,ww,hh,px2,py2);
}


//  create a new PXB pixmap from a subsection of an existing PXB

PXB * PXB_subpxb(PXB *pxb1, int px1, int py1, int ww, int hh)
{
   PXB *pxb2 = PXB_make(ww,hh,pxb1->nc);
   if (! pxb2) return 0;
   PXB_copy_area(pxb1,px1,py1,ww,hh,pxb2,0,0);
   return pxb2;
}


/********************************************************************************/

//  rescale a PXB pixmap image to 1/2 size using NWT threads
//  if rows or columns are odd, the last one is omitted

typedef struct {
   uint8    *pixels1, *pixels2;
   int      ww1, hh1, rs1, nc1;
   int      ww2, hh2, rs2, nc2;
}
   pxb_half_data_t;


PXB * PXB_half(PXB *pxb1)
{
   void * pxb_half_thread(void *arg);
   
   pxb_half_data_t   P;

   PXB  *pxb2;

   P.ww1 = pxb1->ww;                                                             //  input image data
   P.hh1 = pxb1->hh;
   P.rs1 = pxb1->rs;
   P.nc1 = pxb1->nc;
   P.pixels1 = pxb1->pixels;
   
   P.ww2 = P.ww1 / 2;                                                            //  output image data
   P.hh2 = P.hh1 / 2;
   P.nc2 = P.nc1;
   
   pxb2 = PXB_make(P.ww2,P.hh2,P.nc2);                                           //  output PXB
   P.pixels2 = pxb2->pixels;
   P.rs2 = pxb2->rs;

   void        *args[NWT][2];
   pthread_t   tid[NWT];
   
   for (int ii = 0; ii < NWT; ii++) {
      args[ii][0] = &P;
      args[ii][1] = &Nval[ii];
      tid[ii] = start_Jthread(pxb_half_thread,args[ii]);
   }
   
   for (int ii = 0; ii < NWT; ii++)
      wait_Jthread(tid[ii]);

   return pxb2;
}


void * pxb_half_thread(void *arg)
{
   int         row, col, ch, nc, rgb;
   uint8       *pix1a, *pix1b, *pix2;

   void ** args = (void **) arg;
   pxb_half_data_t *P = (pxb_half_data_t *) args[0];
   int   index = * ((int *) args[1]);
   
   nc = P->nc1;

   for (row = index; row < P->hh2; row += NWT)                                   //  loop output rows
   {
      pix1a = P->pixels1 + P->rs1 * row * 2;                                     //  2 input rows
      pix1b = pix1a + P->rs1;
      pix2 = P->pixels2 + P->rs2 * row;                                          //  output row

      for (col = 0; col < P->ww2 * nc; col += nc)                                //  output col
      {
         for (ch = 0; ch < nc; ch++)                                             //  loop channels
         {
            rgb = pix1a[ch] + pix1a[ch+nc] + pix1b[ch] + pix1b[ch+nc];           //  4 input RGB[A] values
            pix2[ch] = rgb / 4;                                                  //    --> 1 output RGB[A]
         }
         pix1a += 2 * nc;
         pix1b += 2 * nc;
         pix2 += nc;
      }
   }

   pthread_exit(0);
}


/********************************************************************************

   Rescale PXB pixmap image to new width and height.                                   19.0
   The scale ratios may be different for width and height.

   Method is better than bilinear:
      For all input pixels [p] having some overlap area with an output pixel:
         output pixel = sum ( overlap[p] * input[p] ) / sum ( overlap[p] )

*********************************************************************************/

typedef struct {
   uint8    *pixels1;
   uint8    *pixels2;
   int      ww1, hh1, rs1;
   int      ww2, hh2, rs2;
   int      nc1, nc2;
   int      *px1L,  *py1L;
   float    *pxmap, *pymap;
   int      maxmapx, maxmapy;
}  
   pxb_rescale_data_t;


PXB * PXB_rescale(PXB *pxb1, int ww, int hh)
{
   void   * pxb_rescale_thread(void *arg);

   pxb_rescale_data_t   P;

   PXB      *pxb2;
   int      px1, py1, px2, py2;
   int      pxl, pyl, pxm, pym, ii;
   float    scalex, scaley;
   float    px1a, py1a, px1b, py1b;
   float    fx, fy;
   int      Fhalf = 0;

   if (ww <= pxb1->ww/2 && hh <= pxb1->hh/2) {                                   //  if scaling down to < 1/2 size,
      pxb1 = PXB_half(pxb1);                                                     //  use fast 1/2 scale down first
      Fhalf = 1;
   }
   
   P.ww1 = pxb1->ww;
   P.hh1 = pxb1->hh;
   P.nc1 = pxb1->nc;
   P.rs1 = pxb1->rs;
   P.pixels1 = pxb1->pixels;
   
   pxb2 = PXB_make(ww,hh,P.nc1);                                                 //  output PXB

   P.ww2 = ww;                                                                   //  output image data
   P.hh2 = hh;
   P.rs2 = pxb2->rs;
   P.nc2 = P.nc1;
   P.pixels2 = pxb2->pixels;
   
   scalex = 1.0 * P.ww1 / P.ww2;                                                 //  compute x and y scales
   scaley = 1.0 * P.hh1 / P.hh2;
   
   if (scalex <= 1) P.maxmapx = 2;                                               //  compute max input pixels
   else P.maxmapx = scalex + 2;                                                  //    mapping into output pixels
   P.maxmapx += 1;                                                               //      for both dimensions
   if (scaley <= 1) P.maxmapy = 2;                                               //  (pixels may not be square)
   else P.maxmapy = scaley + 2;
   P.maxmapy += 1;                                                               //  (extra entry for -1 flag)
   
   P.pymap = (float *) zmalloc(P.hh2 * P.maxmapy * sizeof(float));               //  maps overlap of < maxmap input
   P.pxmap = (float *) zmalloc(P.ww2 * P.maxmapx * sizeof(float));               //    pixels per output pixel

   P.py1L = (int *) zmalloc(P.hh2 * sizeof(int));                                //  maps first (lowest) input pixel
   P.px1L = (int *) zmalloc(P.ww2 * sizeof(int));                                //    per output pixel

   for (py2 = 0; py2 < P.hh2; py2++)                                             //  loop output y-pixels
   {
      py1a = py2 * scaley;                                                       //  corresponding input y-pixels
      py1b = py1a + scaley;
      if (py1b >= P.hh1) py1b = P.hh1 - 0.001;                                   //  fix precision limitation
      pyl = py1a;
      P.py1L[py2] = pyl;                                                         //  1st overlapping input pixel

      for (py1 = pyl, pym = 0; py1 < py1b; py1++, pym++)                         //  loop overlapping input pixels
      {
         if (py1 < py1a) {                                                       //  compute amount of overlap
            if (py1+1 < py1b) fy = py1+1 - py1a;                                 //    0.0 to 1.0 
            else fy = scaley;
         }
         else if (py1+1 > py1b) fy = py1b - py1;
         else fy = 1;

         ii = py2 * P.maxmapy + pym;                                             //  save it
         P.pymap[ii] = 0.9999 * fy / scaley;
      }

      ii = py2 * P.maxmapy + pym;                                                //  set an end marker after
      P.pymap[ii] = -1;                                                          //    last overlapping pixel
   }
   
   for (px2 = 0; px2 < P.ww2; px2++)                                             //  do same for x-pixels
   {
      px1a = px2 * scalex;
      px1b = px1a + scalex;
      if (px1b >= P.ww1) px1b = P.ww1 - 0.001;
      pxl = px1a;
      P.px1L[px2] = pxl;

      for (px1 = pxl, pxm = 0; px1 < px1b; px1++, pxm++)
      {
         if (px1 < px1a) {
            if (px1+1 < px1b) fx = px1+1 - px1a;
            else fx = scalex;
         }
         else if (px1+1 > px1b) fx = px1b - px1;
         else fx = 1;

         ii = px2 * P.maxmapx + pxm;
         P.pxmap[ii] = 0.9999 * fx / scalex;
      }

      ii = px2 * P.maxmapx + pxm;
      P.pxmap[ii] = -1;
   }
   
   void        *args[NWT][2];
   pthread_t   tid[NWT];
   
   for (ii = 0; ii < NWT; ii++) {
      args[ii][0] = &P;
      args[ii][1] = &Nval[ii];
      tid[ii] = start_Jthread(pxb_rescale_thread,args[ii]);
   }
   
   for (ii = 0; ii < NWT; ii++)
      wait_Jthread(tid[ii]);

   zfree(P.px1L);
   zfree(P.py1L);
   zfree(P.pxmap);
   zfree(P.pymap);
   
   if (Fhalf) PXB_free(pxb1);

   return pxb2;
}


void * pxb_rescale_thread(void *arg)                                             //  worker thread function
{
   int         px1, py1, px2, py2;
   int         pxl, pyl, pxm, pym, ii;
   uint8       *pix1, *pix2;
   float       fx, fy, ftot;
   float       red, green, blue, alpha;

   void ** args = (void **) arg;
   pxb_rescale_data_t *P = (pxb_rescale_data_t *) args[0];
   int   index = * ((int *) args[1]);
   
   for (py2 = index; py2 < P->hh2; py2 += NWT)                                   //  loop output y-pixels
   {
      pyl = P->py1L[py2];                                                        //  corresp. 1st input y-pixel

      for (px2 = 0; px2 < P->ww2; px2++)                                         //  loop output x-pixels
      {
         pxl = P->px1L[px2];                                                     //  corresp. 1st input x-pixel

         red = green = blue = alpha = 0;                                         //  initz. output pixel

         for (py1 = pyl, pym = 0; ; py1++, pym++)                                //  loop overlapping input y-pixels
         {
            ii = py2 * P->maxmapy + pym;                                         //  get y-overlap
            fy = P->pymap[ii];
            if (fy < 0) break;                                                   //  no more pixels

            for (px1 = pxl, pxm = 0; ; px1++, pxm++)                             //  loop overlapping input x-pixels
            {
               ii = px2 * P->maxmapx + pxm;                                      //  get x-overlap
               fx = P->pxmap[ii];
               if (fx < 0) break;                                                //  no more pixels

               ftot = fx * fy;                                                   //  area overlap = x * y overlap
               pix1 = P->pixels1 + py1 * P->rs1 + px1 * P->nc1;
               red += pix1[0] * ftot;                                            //  add input pixel * overlap
               green += pix1[1] * ftot;
               blue += pix1[2] * ftot;
               if (P->nc1 == 4) alpha += pix1[3] * ftot;
            }

            pix2 = P->pixels2 + py2 * P->rs2 + px2 * P->nc2;                     //  save output pixel
            pix2[0] = red;
            pix2[1] = green;
            pix2[2] = blue;
            if (P->nc2 == 4) pix2[3] = alpha;
         }
      }
   }

   pthread_exit(0);
}


/********************************************************************************/

//  resize PXB to given max. width or height, preserving aspect ratio
//  same as PXB_rescale() but one size argument instead of width and height

PXB * PXB_resize(PXB *pxb1, int size)
{
   int      ww1, hh1, ww2, hh2;
   PXB      *pxb2;
   
   ww1 = pxb1->ww;
   hh1 = pxb1->hh;
   
   if (ww1 >= hh1) {
      ww2 = size;
      hh2 = 1.0 * size * hh1 / ww1 + 0.5;
   }
   else {
      hh2 = size;
      ww2 = 1.0 * size * ww1 / hh1 + 0.5;
   }
   
   pxb2 = PXB_rescale(pxb1,ww2,hh2);
   return pxb2;
}


/********************************************************************************/

//  fast PXB pixmap rescale using 'half binomial' interpolation
//  works best when the rescale ratio is between 0.5 and 2.0

typedef struct {
   int      ww1, hh1, ww2, hh2, rs1, rs2, nc1, nc2;
   uint8    *pixels1, *pixels2;
   float    xscale, yscale;
   int      nwt;
}
   pxb_rescale_fast_data_t;


PXB * PXB_rescale_fast(PXB *pxb1, int ww, int hh)
{
   void * pxb_rescale_fast_thread(void *arg);
   
   pxb_rescale_fast_data_t  P;

   PXB      *pxb2;

   P.ww1 = pxb1->ww;                                                             //  input image data
   P.hh1 = pxb1->hh;
   P.rs1 = pxb1->rs;
   P.nc1 = pxb1->nc;
   P.pixels1 = pxb1->pixels;

   P.ww2 = ww;                                                                   //  output image data
   P.hh2 = hh;
   P.nc2 = P.nc1;
   pxb2 = PXB_make(P.ww2,P.hh2,P.nc2);
   P.rs2 = pxb2->rs;
   P.pixels2 = pxb2->pixels;

   P.xscale = 1.0 * (P.ww1-1) / P.ww2;
   P.yscale = 1.0 * (P.hh1-1) / P.hh2;
   
   P.nwt = NWT;                                                                  //  limit to 4 threads                 20.0
   if (P.nwt > 4) P.nwt = 4;                                                     //  (more not helpful)
   
   void        *args[NWT][2];
   pthread_t   tid[NWT];

   for (int ii = 0; ii < P.nwt; ii++) {                                          //  start working threads
      args[ii][0] = &P;
      args[ii][1] = &Nval[ii];
      tid[ii] = start_Jthread(pxb_rescale_fast_thread,args[ii]);
   }

   for (int ii = 0; ii < P.nwt; ii++)                                            //  wait for all done
      wait_Jthread(tid[ii]);

   return pxb2;
}


void * pxb_rescale_fast_thread(void *arg)
{
   int      px1, py1, px2, py2;
   uint8    *pix10, *pix11, *pix2;

   void ** args = (void **) arg;
   int   index = * ((int *) args[1]);

   set_cpu_affinity(index);                                                      //  stay on same cpu if possible       20.0

   pxb_rescale_fast_data_t *P = (pxb_rescale_fast_data_t *) args[0];

   for (py2 = index; py2 < P->hh2; py2 += P->nwt)                                //  loop output pixels
   for (px2 = 0; px2 < P->ww2; px2++)
   {
      px1 = px2 * P->xscale;                                                     //  input pixel corresponding
      py1 = py2 * P->yscale;                                                     //    to output pixel

      pix10 = P->pixels1 + py1 * P->rs1 + px1 * P->nc1;                          //  input pixel RGB data
      pix11 = pix10 + P->nc1;                                                    //  next pixel right
      
      pix2 = P->pixels2 + py2 * P->rs2 + px2 * P->nc2;                           //  output pixel

      pix2[0] = (pix10[0] + pix11[0]) / 2;
      pix2[1] = (pix10[1] + pix11[1]) / 2;
      pix2[2] = (pix10[2] + pix11[2]) / 2;
   }

   pthread_exit(0);
}


/********************************************************************************/

//  fast resize PXB to given max. width or height, preserving aspect ratio
//  same as PXB_rescale_fast() but one size argument instead of width and height

PXB * PXB_resize_fast(PXB *pxb1, int size)
{
   int      ww1, hh1, ww2, hh2;
   PXB      *pxb2;
   
   ww1 = pxb1->ww;
   hh1 = pxb1->hh;
   
   if (ww1 >= hh1) {
      ww2 = size;
      hh2 = 1.0 * size * hh1 / ww1 + 0.5;
   }
   else {
      hh2 = size;
      ww2 = 1.0 * size * ww1 / hh1 + 0.5;
   }
   
   pxb2 = PXB_rescale_fast(pxb1,ww2,hh2);
   return pxb2;
}


/********************************************************************************

   PXB * PXB_rotate(PXB *pxb, float angle)

   Rotate PXB pixmap image through any angle (degrees).

   The returned pixmap has the same size as the original, but the
   pixmap envelope is increased to accommodate the rotated original
   (e.g. a 100x100 pixmap rotated 45 deg. needs a 142x142 pixmap).

   Pixels added around the rotated pixmap have RGB[A] = 0.
   Angle is in degrees. Positive direction is clockwise.
   PXB must have 3 or 4 channels (RGB or RGBA).
   Loss of resolution is about 1/2 pixel.

   Algorithm:
      create output PXB big enough for rotated input PXB
      compute coefficients for affine transform
      loop all output pixels (px2,py2)
         get corresp. input pixel (px1,py1) using affine transform
         if outside of PXB pixmap
            output pixel = black
            continue
         for 4 input pixels based at (px0,py0) = (int(px1),int(py1))
            compute overlap (0 to 1) with (px1,py1)
            sum RGB values * overlap
         output aggregate RGB to pixel (px2,py2)

*********************************************************************************/

PXB * PXB_rotate(PXB *pxb1, float angle)
{
   PXB      *pxb2;
   int      nc, ac;
   int      ww1, hh1, rs1, ww2, hh2, rs2;
   int      px2, py2, px0, py0;
   float    px1, py1;
   float    f0, f1, f2, f3, red, green, blue, alpha = 0;
   float    a, b, d, e, ww15, hh15, ww25, hh25;
   uchar    *ppix1, *ppix2, *pix0, *pix1, *pix2, *pix3;

   ww1 = pxb1->ww;
   hh1 = pxb1->hh;
   nc = pxb1->nc;
   ac = 0;
   if (nc == 4) ac = 1;                                                          //  has alpha channel
   rs1 = ww1 * nc;
   
   while (angle < -180) angle += 360;                                            //  normalize, -180 to +180
   while (angle > 180) angle -= 360;
   angle = angle * PI / 180;                                                     //  radians, -PI to +PI

   if (fabsf(angle) < 0.001) {
      pxb2 = PXB_copy(pxb1);                                                     //  angle is zero within my precision
      return pxb2;
   }

   ww2 = int(ww1*fabsf(cosf(angle)) + hh1*fabsf(sinf(angle)));                   //  rectangle containing rotated pixmap
   hh2 = int(ww1*fabsf(sinf(angle)) + hh1*fabsf(cosf(angle)));

   pxb2 = PXB_make(ww2,hh2,nc);                                                  //  create output pxb2
   if (! pxb2) return 0;                                                         //  initially RGBA = 0
   rs2 = ww2 * nc;
   
   ppix1 = pxb1->pixels;
   ppix2 = pxb2->pixels;

   ww15 = 0.5 * ww1;
   hh15 = 0.5 * hh1;
   ww25 = 0.5 * ww2;
   hh25 = 0.5 * hh2;

   a = cosf(angle);                                                              //  affine transform coefficients
   b = sinf(angle);
   d = - sinf(angle);
   e = cosf(angle);

   for (py2 = 0; py2 < hh2; py2++)                                               //  loop through output pixels
   for (px2 = 0; px2 < ww2; px2++)
   {
      px1 = a * (px2 - ww25) + b * (py2 - hh25) + ww15;                          //  (px1,py1) = corresponding
      py1 = d * (px2 - ww25) + e * (py2 - hh25) + hh15;                          //    point within input pixels

      px0 = int(px1);                                                            //  pixel containing (px1,py1)
      py0 = int(py1);

      if (px0 < 0 || px0 >= ww1-1 || py0 < 0 || py0 >= hh1-1)                    //  if outside input pixel array,
         continue;                                                               //    leave black

      pix0 = ppix1 + py0 * rs1 + px0 * nc;                                       //  4 input pixels based at (px0,py0)
      pix1 = pix0 + rs1;
      pix2 = pix0 + nc;
      pix3 = pix1 + nc;
      
      f0 = (px0+1 - px1) * (py0+1 - py1);                                        //  overlap of (px1,py1)
      f1 = (px0+1 - px1) * (py1 - py0);                                          //    in each of the 4 pixels
      f2 = (px1 - px0) * (py0+1 - py1);
      f3 = (px1 - px0) * (py1 - py0);

      red =   f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];         //  sum the weighted inputs
      green = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
      blue =  f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];
      if (ac) alpha = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3]; 

      pix2 = ppix2 + py2 * rs2 + px2 * nc;                                       //  output pixel
      pix2[0] = int(red);
      pix2[1] = int(green);
      pix2[2] = int(blue);
      if (ac) pix2[3] = int(alpha);
   }

   return pxb2;
}


/********************************************************************************/

//  Get a virtual pixel at location (px,py) (real) in a PXB pixmap.
//  Get the overlapping integer pixels and build a composite.
//  Output vpix is uint8[4] supplied by caller.
//  Returns 1 if OK, 0 if px/py out of limits for pxm.

int vpixel(PXB *pxb, float px, float py, uint8 *vpix)
{
   int      ww, hh, rs, nc, ac, px0, py0;
   uint8    *pix0, *pix1, *pix2, *pix3;
   float    f0, f1, f2, f3;

   ww = pxb->ww;
   hh = pxb->hh;
   rs = pxb->rs;
   nc = pxb->nc;
   ac = nc - 3;

   px0 = px;                                                                     //  integer pixel containing (px,py)
   py0 = py;

   if (px0 < 0 || py0 < 0) return 0;
   if (px0 > ww-2 || py0 > hh-2) return 0;

   f0 = (px0+1 - px) * (py0+1 - py);                                             //  overlap of (px,py)
   f1 = (px0+1 - px) * (py - py0);                                               //   in each of the 4 pixels
   f2 = (px - px0) * (py0+1 - py);
   f3 = (px - px0) * (py - py0);

   pix0 = PXBpix(pxb,px0,py0);                                                   //  pixel (px0,py0)
   pix1 = pix0 + rs;                                                             //        (px0,py0+1)
   pix2 = pix0 + nc;                                                             //        (px0+1,py0)
   pix3 = pix1 + nc;                                                             //        (px0+1,py0+1)

   vpix[0] = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];          //  sum the weighted inputs
   vpix[1] = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
   vpix[2] = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];
   if (ac) vpix[3] = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3];

   return 1;
}


//  Get a virtual pixel at location (px,py) (real) in a PXM pixmap.
//  Get the overlapping float pixels and build a composite.
//  Output vpix is float[4] supplied by caller.
//  Returns 1 if OK, 0 if px/py out of limits for pxm.

int vpixel(PXM *pxm, float px, float py, float *vpix)
{
   int      ww, hh, nc, ac, px0, py0;
   float    *pix0, *pix1, *pix2, *pix3;
   float    f0, f1, f2, f3;

   ww = pxm->ww;
   hh = pxm->hh;
   nc = pxm->nc;
   ac = nc - 3;

   px0 = px;                                                                     //  integer pixel containing (px,py)
   py0 = py;

   if (px0 < 1 || py0 < 1) return 0;                                             //  off-image or edge pixel
   if (px0 > ww-2 || py0 > hh-2) return 0;

   f0 = (px0+1 - px) * (py0+1 - py);                                             //  overlap of (px,py)
   f1 = (px0+1 - px) * (py - py0);                                               //   in each of the 4 pixels
   f2 = (px - px0) * (py0+1 - py);
   f3 = (px - px0) * (py - py0);

   pix0 = PXMpix(pxm,px0,py0);                                                   //  pixel (px0,py0)
   pix1 = pix0 + ww * nc;                                                        //        (px0,py0+1)
   pix2 = pix0 + nc;                                                             //        (px0+1,py0)
   pix3 = pix1 + nc;                                                             //        (px0+1,py0+1)

   vpix[0] = f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];          //  sum the weighted inputs
   vpix[1] = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
   vpix[2] = f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];

   if (ac)                                                                       //  alpha channel
      vpix[3] = f0 * pix0[3] + f1 * pix1[3] + f2 * pix2[3] + f3 * pix3[3];
                                                                                 //  remove test of vpix[2] < 1
   return 1;
}


/********************************************************************************/

//  Copy a PXM pixmap image (RGB float) to a PXB pixmap image (RGB uint8).
//  NOT THREAD SAFE

namespace PXM_PXB_copy_names
{
   PXM      *pxm1;
   PXB      *pxb2;
   int      ww, hh, nc;
}


PXB * PXM_PXB_copy(PXM *pxm)
{
   using namespace PXM_PXB_copy_names;

   void * PXM_PXB_copy_thread(void *);

   ww = pxm->ww;
   hh = pxm->hh;
   nc = pxm->nc;

   pxm1 = pxm;
   pxb2 = PXB_make(ww,hh,nc);

   do_wthreads(PXM_PXB_copy_thread,NWT);

   return pxb2;
}


void * PXM_PXB_copy_thread(void *arg)                                            //  speedup with threads               19.0
{
   using namespace PXM_PXB_copy_names;

   int      index = *((int *) arg);
   int      px, py;
   float    *pix1;
   uint8    *pix2;

   for (py = index; py < hh; py += NWT)
   {
      pix1 = PXMpix(pxm1,0,py);
      pix2 = PXBpix(pxb2,0,py);

      for (px = 0; px < ww; px++)
      {
         pix2[0] = pix1[0];
         pix2[1] = pix1[1];
         pix2[2] = pix1[2];
         if (nc == 4) pix2[3] = pix1[3];
         pix1 += nc;
         pix2 += nc;
      }
   }

   pthread_exit(0);
}


//  Update a PXB section from an updated PXM section.
//  PXM and PXB must have the same dimensions.
//  px3, py3, ww3, hh3: modified section within pxm1 to propagate to pxb2;

void PXM_PXB_update(PXM *pxm1, PXB *pxb2, int px3, int py3, int ww3, int hh3)
{
   float    *pix1;
   uint8    *pix2;
   int      px, py;
   int      lox, hix, loy, hiy;
   int      nc1, nc2, ac;
   
   if (pxm1->ww + pxm1->hh != pxb2->ww + pxb2->hh) {
      printz("PXM_PXB_update() call error %d %d %d %d \n",
               pxm1->ww, pxm1->hh, pxb2->ww, pxb2->hh);
      return;
   }

   lox = px3;
   hix = px3 + ww3;
   if (lox < 0) lox = 0;
   if (hix > pxb2->ww) hix = pxb2->ww;

   loy = py3;
   hiy = py3 + hh3;
   if (loy < 0) loy = 0;
   if (hiy > pxb2->hh) hiy = pxb2->hh;
   
   nc1 = pxm1->nc;
   nc2 = pxb2->nc;
   
   if (nc1 > 3 && nc2 > 3) ac = 1;                                               //  alpha channel
   else ac = 0;
   
   for (py = loy; py < hiy; py++)
   {
      pix1 = PXMpix(pxm1,lox,py);
      pix2 = PXBpix(pxb2,lox,py);

      for (px = lox; px < hix; px++)
      {
         pix2[0] = pix1[0];                                                      //  0.0 - 255.99  >>  0 - 255
         pix2[1] = pix1[1];
         pix2[2] = pix1[2];
         if (ac) pix2[3] = pix1[3];
         pix1 += nc1;
         pix2 += nc2;
      }
   }

   return;
}


//  Update an output PXB section from a corresponding input PXB section.
//  The two PXBs represent the same image at different scales (width/height).
//  Input pxb1 must be larger or equal to output pxb2.
//  px3, py3, ww3, hh3: modified section within pxb1 to propagate to pxb2;

void PXB_PXB_update(PXB *pxb1, PXB *pxb2, int px3, int py3, int ww3, int hh3)
{
   static int     pww1 = 0, phh1 = 0, pww2 = 0, phh2 = 0;
   static int     *px1L = 0, *py1L = 0;
   static float   scalex = 1, scaley = 1;
   static float   *pxmap = 0, *pymap = 0;
   static int     maxmapx = 0, maxmapy = 0;

   uint8       *ppix1, *ppix2;
   int         ww1, hh1, ww2, hh2, rs1, rs2, nc1, nc2, ac;
   int         px1, py1, px2, py2;
   int         pxl, pyl, pxm, pym, ii;
   float       px1a, py1a, px1b, py1b;
   float       fx, fy, ftot;
   uint8       *pixel1, *pixel2;
   float       red, green, blue, alpha;
   int         lox, hix, loy, hiy;

   ww1 = pxb1->ww;
   hh1 = pxb1->hh;
   rs1 = pxb1->rs;
   nc1 = pxb1->nc;
   ppix1 = pxb1->pixels;

   ww2 = pxb2->ww;
   hh2 = pxb2->hh;
   rs2 = pxb2->rs;
   nc2 = pxb2->nc;
   ppix2 = pxb2->pixels;
   
   if (nc1 > 3 && nc2 > 3) ac = 1;                                               //  alpha channel
   else ac = 0;

   if (ww1 == pww1 && hh1 == phh1 && ww2 == pww2 && hh2 == phh2)                 //  if the sizes are the same as before,
      goto copy_pixels;                                                          //    the pixel mapping math can be avoided.

   pww1 = ww1;
   phh1 = hh1;
   pww2 = ww2;
   phh2 = hh2;

   if (px1L) {                                                                   //  unless this is the first call,
      zfree(px1L);                                                               //    free prior map memory
      zfree(py1L);
      zfree(pxmap);
      zfree(pymap);
   }

   scalex = 1.0 * ww1 / ww2;                                                     //  compute x and y scales
   scaley = 1.0 * hh1 / hh2;

   if (scalex <= 1) maxmapx = 2;                                                 //  compute max input pixels
   else maxmapx = scalex + 2;                                                    //    mapping into output pixels
   maxmapx += 1;                                                                 //      for both dimensions
   if (scaley <= 1) maxmapy = 2;                                                 //  (pixels may not be square)
   else maxmapy = scaley + 2;
   maxmapy += 1;                                                                 //  (extra entry for -1 flag)

   pymap = (float *) zmalloc(hh2 * maxmapy * sizeof(float));                     //  maps overlap of < maxmap input
   pxmap = (float *) zmalloc(ww2 * maxmapx * sizeof(float));                     //    pixels per output pixel

   py1L = (int *) zmalloc(hh2 * sizeof(int));                                    //  maps first (lowest) input pixel
   px1L = (int *) zmalloc(ww2 * sizeof(int));                                    //    per output pixel

   for (py2 = 0; py2 < hh2; py2++)                                               //  loop output y-pixels
   {
      py1a = py2 * scaley;                                                       //  corresponding input y-pixels
      py1b = py1a + scaley;
      if (py1b >= hh1) py1b = hh1 - 0.001;                                       //  fix precision limitation
      pyl = py1a;
      py1L[py2] = pyl;                                                           //  1st overlapping input pixel

      for (py1 = pyl, pym = 0; py1 < py1b; py1++, pym++)                         //  loop overlapping input pixels
      {
         if (py1 < py1a) {                                                       //  compute amount of overlap
            if (py1+1 < py1b) fy = py1+1 - py1a;                                 //    0.0 to 1.0
            else fy = scaley;
         }
         else if (py1+1 > py1b) fy = py1b - py1;
         else fy = 1;

         ii = py2 * maxmapy + pym;                                               //  save it
         pymap[ii] = 0.9999 * fy / scaley;
      }
      ii = py2 * maxmapy + pym;                                                  //  set an end marker after
      pymap[ii] = -1;                                                            //    last overlapping pixel
   }

   for (px2 = 0; px2 < ww2; px2++)                                               //  do same for x-pixels
   {
      px1a = px2 * scalex;
      px1b = px1a + scalex;
      if (px1b >= ww1) px1b = ww1 - 0.001;
      pxl = px1a;
      px1L[px2] = pxl;

      for (px1 = pxl, pxm = 0; px1 < px1b; px1++, pxm++)
      {
         if (px1 < px1a) {
            if (px1+1 < px1b) fx = px1+1 - px1a;
            else fx = scalex;
         }
         else if (px1+1 > px1b) fx = px1b - px1;
         else fx = 1;

         ii = px2 * maxmapx + pxm;
         pxmap[ii] = 0.9999 * fx / scalex;
      }
      ii = px2 * maxmapx + pxm;
      pxmap[ii] = -1;
   }

copy_pixels:

   px3 = px3 / scalex;                                                           //  convert input area to output area
   py3 = py3 / scaley;
   ww3 = ww3 / scalex + 2;
   hh3 = hh3 / scaley + 2;

   lox = px3;
   hix = px3 + ww3;
   if (lox < 0) lox = 0;
   if (hix > ww2) hix = ww2;

   loy = py3;
   hiy = py3 + hh3;
   if (loy < 0) loy = 0;
   if (hiy > hh2) hiy = hh2;

   for (py2 = loy; py2 < hiy; py2++)                                             //  loop output y-pixels
   {
      pyl = py1L[py2];                                                           //  corresp. 1st input y-pixel

      for (px2 = lox; px2 < hix; px2++)                                          //  loop output x-pixels
      {
         pxl = px1L[px2];                                                        //  corresp. 1st input x-pixel

         red = green = blue = alpha = 0;                                         //  initz. output pixel

         for (py1 = pyl, pym = 0; ; py1++, pym++)                                //  loop overlapping input y-pixels
         {
            ii = py2 * maxmapy + pym;                                            //  get y-overlap
            fy = pymap[ii];
            if (fy < 0) break;                                                   //  no more pixels

            for (px1 = pxl, pxm = 0; ; px1++, pxm++)                             //  loop overlapping input x-pixels
            {
               ii = px2 * maxmapx + pxm;                                         //  get x-overlap
               fx = pxmap[ii];
               if (fx < 0) break;                                                //  no more pixels

               ftot = fx * fy;                                                   //  area overlap = x * y overlap
               pixel1 = ppix1 + py1 * rs1 + px1 * nc1;
               red += pixel1[0] * ftot;                                          //  add input pixel * overlap
               green += pixel1[1] * ftot;
               blue += pixel1[2] * ftot;
               if (ac) alpha += pixel1[3] * ftot;
            }
         }

         pixel2 = ppix2 + py2 * rs2 + px2 * nc2;                                 //  save output pixel
         pixel2[0] = red;                                                        //  0.0 - 255.996  >>  0 - 255
         pixel2[1] = green;
         pixel2[2] = blue;
         if (ac) pixel2[3] = alpha;
      }
   }

   return;
}


/********************************************************************************

   primary image file read and write functions

   PXB pixmap <--> file    8-bit RGB[A]
   PXM pixmap <--> file    float RGB[A]
   
*********************************************************************************/

//  Load an image file into a PXB pixmap (8 bit color).
//  Also sets the following file scope variables:
//    f_load_type = "jpg" "tif" "png" "RAW" "video" "other"
//    f_load_bpc = disk file bits per color = 8/16
//    f_load_size = disk file size
//  If Fack is true, failure will cause a popup ACK dialog
//  Do not call from a thread with Fack true.

PXB * PXB_load(cchar *file, int Fack)
{
   int      err;
   FTYPE    ftype;
   char     *file2 = 0, *framefile;
   cchar    *pext;
   PXB      *pxb = 0;
   STATB    statb;
   
   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) {
      printz("%s %s \n",Bfilenotfound,file);                                     //  20.0
      goto errret;
   }

   f_load_size = statb.st_size;
   f_load_bpc = 8;                                                               //  default
   
   ftype = image_file_type(file);
   if (ftype != IMAGE && ftype != RAW && ftype != VIDEO) {
      printz("file type not supported: %s \n",file);
      goto errret;
   }
   
   if (ftype == VIDEO)                                                           //  video file - extract frame 1
   {
      framefile = zstrdup(temp_folder,20);                                       //  output .jpg file
      strcat(framefile,"/frame1.jpg");

      file2 = zescape_quotes(file);
      err = shell_quiet("ffmpeg -i \"%s\" -v 8 -frames 1 -y %s",file2,framefile);
      zfree(file2);

      if (err) {
         printz("cannot get video frame 1: %s %s \n",file,strerror(err));
         goto errret;
      }
      
      pxb = ANY_PXB_load(framefile);
      if (! pxb) goto errret;

      strcpy(f_load_type,"video");
   }

   if (ftype == IMAGE) 
   {
      pext = strrchr(file,'/');
      if (! pext) pext = file;
      pext = strrchr(pext,'.');
      if (! pext) pext = "";

      if (strcasestr(".jpg .jpeg",pext)) {
         pxb = JPG_PXB_load(file);
         strcpy(f_load_type,"jpg");
      }
      
      else if (strcasestr(".tif .tiff",pext)) {
         pxb = TIFF_PXB_load(file);
         strcpy(f_load_type,"tif");
      }

      else if (strcasestr(".png",pext)) {
         pxb = PNG_PXB_load(file);
         strcpy(f_load_type,"png");
      }

      else if (strcasestr(".heic",pext)) {                                       //  20.0
         pxb = HEIC_PXB_load(file);
         strcpy(f_load_type,"heic");
      }

      else if (strcasestr(".jp2",pext)) {                                        //  20.0
         pxb = JP2_PXB_load(file);
         strcpy(f_load_type,"jp2");
      }

      else {
         pxb = ANY_PXB_load(file);
         strcpy(f_load_type,"other");
      }
   }

   if (ftype == RAW) {
      pxb = RAW_PXB_load(file,Fautobright,Fmatchthumb);
      strcpy(f_load_type,"RAW");
   }

   if (pxb) return pxb;

errret:
   strcpy(f_load_type,"none");
   f_load_bpc = f_load_size = 0;
   if (Fack) zmessageACK(Mwin,"file error: %s (see log file)",file);
   return 0;
}


/********************************************************************************/

//  Load an image file into a PXM pixmap (float color).
//  Also sets the following file scope variables:
//    f_load_type = "jpg" "tif" "png" "RAW" "other"
//    f_load_bpc = disk file bits per color = 8/16
//    f_load_size = disk file size
//  If Fack is true, failure will cause a popup ACK dialog
//  Do not call from a thread with Fack true.

PXM * PXM_load(cchar *file, int Fack)
{
   int      err;
   FTYPE    ftype;
   cchar    *pext;
   PXM      *pxm = 0;
   STATB    statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) {
      printz("%s %s \n",Bfilenotfound,file);
      goto errret;
   }

   f_load_size = statb.st_size;
   f_load_bpc = 8;                                                               //  default

   ftype = image_file_type(file);
   if (ftype != IMAGE && ftype != RAW) {
      printz("file type not supported: %s \n",file);
      goto errret;
   }
   
   if (ftype == IMAGE) 
   {
      pext = strrchr(file,'/');
      if (! pext) pext = file;
      pext = strrchr(pext,'.');
      if (! pext) pext = "";

      if (strcasestr(".jpg .jpeg",pext)) {
         pxm = JPG_PXM_load(file);
         strcpy(f_load_type,"jpg");
      }
      
      else if (strcasestr(".tif .tiff",pext)) {
         pxm = TIFF_PXM_load(file);
         strcpy(f_load_type,"tif");
      }
         
      else if (strcasestr(".png",pext)) {
         pxm = PNG_PXM_load(file);
         strcpy(f_load_type,"png");
      }

      else if (strcasestr(".heic",pext)) {                                       //  20.0
         pxm = HEIC_PXM_load(file);
         strcpy(f_load_type,"heic");
      }

      else if (strcasestr(".jp2",pext)) {                                        //  20.0
         pxm = JP2_PXM_load(file);
         strcpy(f_load_type,"jp2");
      }

      else {
         pxm = ANY_PXM_load(file);
         strcpy(f_load_type,"other");
      }
   }

   if (ftype == RAW) {
      pxm = RAW_PXM_load(file,Fautobright,Fmatchthumb);
      strcpy(f_load_type,"RAW");
   }

   if (pxm) return pxm;

errret:
   strcpy(f_load_type,"none");
   f_load_bpc = f_load_size = 0;
   if (Fack) zmessageACK(Mwin,"file error: %s (see log file)",file);
   return 0;
}


/********************************************************************************/

//  Save a PXB pixmap to a specified file, bits/color, and quality level.
//  If output file is JPEG, bpc is 8 and quality is used for compression level.
//  If output file is TIFF or PNG, bpc may be 8 or 16 and quality is ignored.
//  If Fack is true, failure will cause a popup ACK dialog.
//  Return 0 if OK, +N if error

int PXB_save(PXB *pxb, cchar *file, int bpc, int quality, int Fack)
{
   int      err;
   cchar    *pext;

   pext = strrchr(file,'/');
   if (! pext) pext = file;
   pext = strrchr(pext,'.');
   if (! pext) pext = "";

   if (strcasestr(".tif .tiff",pext)) 
      err = PXB_TIFF_save(pxb,file,bpc);

   else if (strcasestr(".png",pext)) 
      err = PXB_PNG_save(pxb,file,bpc);

   else if (strcasestr(".heic",pext))                                            //  20.0
      err = PXB_HEIC_save(pxb,file);

   else if (strcasestr(".jp2",pext))                                             //  20.0
      err = PXB_JP2_save(pxb,file);

   else  err = PXB_JPG_save(pxb,file,quality);
   if (! err) return 0;

   if (Fack) zmessageACK(Mwin,"file error: %s (see log file)",file);
   return err;
}


/********************************************************************************/

//  Save a PXM pixmap to a specified file, bits/color, and quality level.
//  If output file is JPEG, bpc is 8 and quality is used for compression level.
//  If output file is TIFF or PNG, bpc may be 8 or 16 and quality is ignored.
//  If Fack is true, failure will cause a popup ACK dialog.
//  Return 0 if OK, +N if error

int PXM_save(PXM *pxm, cchar *file, int bpc, int quality, int Fack)
{
   int      err;
   cchar    *pext;

   pext = strrchr(file,'/');
   if (! pext) pext = file;
   pext = strrchr(pext,'.');
   if (! pext) pext = "";

   if (strcasestr(".tif .tiff",pext)) 
      err = PXM_TIFF_save(pxm,file,bpc);

   else if (strcasestr(".png",pext)) 
      err = PXM_PNG_save(pxm,file,bpc);

   else if (strcasestr(".heic",pext))                                            //  20.0
      err = PXM_HEIC_save(pxm,file);

   else if (strcasestr(".jp2",pext))                                             //  20.0
      err = PXM_JP2_save(pxm,file);

   else err = PXM_JPG_save(pxm,file,quality);
   if (! err) return 0;

   if (Fack) zmessageACK(Mwin,"file error: %s (see log file)",file);
   return err;
}


/********************************************************************************
   JPG file read and write functions                                                      19.0
*********************************************************************************/

#pragma GCC push_options                                                         //  stop GCC from removing necessary code
#pragma GCC optimize ("O1")                                                      //  (all calls to jpeg_xxxx functions)


//  JPEG error handler function

typedef struct libjpeg_error_mgr *libjpeg_error_ptr;

struct libjpeg_error_mgr {
   struct jpeg_error_mgr pub;
   jmp_buf setjmp_buffer;
};

void libjpeg_error_exit (j_common_ptr cinfo)
{
   libjpeg_error_ptr errptr = (libjpeg_error_ptr) cinfo->err;
// (*cinfo->err->output_message) (cinfo);                                        //  suppress secondary message
   longjmp(errptr->setjmp_buffer, 1);
}


//  Load PXB pixmap from JPG file using JPG library.
//  set size = 0 to load a full image size
//  set size = N to load an image with width and height <= size

PXB * JPG_PXB_load(cchar *file, int size)                                        //  19.0
{
   struct jpeg_decompress_struct    cinfo;
   struct libjpeg_error_mgr         jerr;

   FILE        *fid = 0;
   uint8       **row_pointers = 0;
   uint8       *pixels = 0;
   uint8       *buffer;
   int         err, ww, hh, num, nc1, nc2, cc, row, nb;
   PXB         *pxb = 0, *pxb2 = 0;
   STATB       statb;
   
   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   fid = fopen(file,"r");
   if (! fid) goto errret;

   cinfo.err = jpeg_std_error(&jerr.pub);                                        //  replace standard error handler
   jerr.pub.error_exit = libjpeg_error_exit;                                     //    which exits on error

   if (setjmp(jerr.setjmp_buffer)) {
      jpeg_destroy_decompress(&cinfo);
      goto errret;
   }
   
   jpeg_create_decompress(&cinfo);                                               //  initz. for read
   jpeg_stdio_src(&cinfo,fid);
   jpeg_read_header(&cinfo,TRUE);
   
   ww = cinfo.image_width;                                                       //  image size
   hh = cinfo.image_height;

   if (size)                                                                     //  used for thumbnails
   {
      if (ww >= hh) {                                                            //  calculate num for actual size
         num = 16 * size / ww;                                                   //    minimally >= target size
         if (ww * num / 16 < size) num++;
      }
      else {
         num = 16 * size / hh;
         if (hh * num / 16 < size) num++;
      }
      if (num == 0) num = 1;
      if (num < 16) {
         cinfo.scale_num = num;                                                  //  target size = num/16 * full size
         cinfo.scale_denom = 16;
      }
   }
   
   jpeg_start_decompress(&cinfo);
   
   nb = cinfo.rec_outbuf_height;                                                 //  rows per read
   nc1 = cinfo.output_components;                                                //  color channels
   if (nc1 != 1 && nc1 != 3) {
      printz("%d colors not supported: %s \n",nc1,file);
      goto errret;
   }
   
   ww = cinfo.output_width;                                                      //  actual output dimensions
   hh = cinfo.output_height;

   cc = ww * hh * nc1;   
   pixels = (uint8 *) zmalloc(cc);                                               //  allocate memory for RGB image

   cc = hh * sizeof(void *);                                                     //  allocate row pointers
   row_pointers = (uint8 **) zmalloc(cc);

   for (row = 0; row < hh; row++)                                                //  initz. row pointers
      row_pointers[row] = (uint8 *) pixels + row * ww * nc1;

   while (cinfo.output_scanline < cinfo.output_height) {                         //  read rows
      row = cinfo.output_scanline;
      buffer = row_pointers[row];
      jpeg_read_scanlines(&cinfo,&buffer,nb);
   }

   jpeg_finish_decompress(&cinfo);                                               //  wrap-up
   jpeg_destroy_decompress(&cinfo);
   fclose(fid);
   zfree(row_pointers);
   row_pointers = 0;

   nc2 = 3;
   pxb = PXB_make(ww,hh,nc2);                                                    //  output PXB, no alpha channel
   
   pixelvert(pixels,pxb->pixels,ww,hh,nc1,nc2);                                  //  convert pixel data

   zfree(pixels);
   pixels = 0;
   
   if (size)                                                                     //  make final exact size
   {
      if (ww >= hh) {
         hh = hh * size / ww;
         ww = size;
      }
      else {
         ww = ww * size / hh;
         hh = size;
      }

      pxb2 = PXB_rescale(pxb,ww,hh);
      PXB_free(pxb);
      pxb = pxb2;
   }
   
   f_load_bpc = 8;
   return pxb;

errret:
   if (errno) printz("error: %s \n",strerror(errno));
   printz("jpeg file error: %s \n",file);
   if (fid && fileno(fid) != -1) fclose(fid);
   if (row_pointers) zfree(row_pointers);
   if (pixels) zfree(pixels);
   return 0;
}


//  Load PXM pixmap from JPG file using JPG library.

PXM * JPG_PXM_load(cchar *file)
{
   struct jpeg_decompress_struct    cinfo;
   struct libjpeg_error_mgr         jerr;

   FILE     *fid = 0;
   uint8    **row_pointers = 0;
   uint8    *pixels = 0;
   uint8    *buffer;
   int      err, ww, hh, nc1, nc2, cc, row, nb;
   PXM      *pxm = 0;
   STATB    statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   fid = fopen(file,"r");
   if (! fid) goto errret;

   cinfo.err = jpeg_std_error(&jerr.pub);                                        //  replace standard error handler
   jerr.pub.error_exit = libjpeg_error_exit;                                     //    which exits on error

   if (setjmp(jerr.setjmp_buffer)) {
      jpeg_destroy_decompress(&cinfo);
      goto errret;
   }
   
   jpeg_create_decompress(&cinfo);                                               //  initz. for read
   jpeg_stdio_src(&cinfo,fid);
   jpeg_read_header(&cinfo,TRUE);
   
   ww = cinfo.image_width;                                                       //  image size
   hh = cinfo.image_height;
   
   jpeg_start_decompress(&cinfo);

   nb = cinfo.rec_outbuf_height;                                                 //  rows per read
   nc1 = cinfo.output_components;                                                //  color channels
   if (nc1 != 1 && nc1 != 3) {
      printz("%d colors not supported: %s \n",nc1,file);
      goto errret;
   }

   ww = cinfo.output_width;                                                      //  actual output dimensions
   hh = cinfo.output_height;

   cc = ww * hh * nc1;   
   pixels = (uint8 *) zmalloc(cc);                                               //  allocate memory for RGB image

   cc = hh * sizeof(void *);                                                     //  allocate row pointers
   row_pointers = (uint8 **) zmalloc(cc);

   for (row = 0; row < hh; row++)                                                //  initz. row pointers
      row_pointers[row] = (uint8 *) pixels + row * ww * nc1;

   while (cinfo.output_scanline < cinfo.output_height) {                         //  read rows
      row = cinfo.output_scanline;
      buffer = row_pointers[row];
      jpeg_read_scanlines(&cinfo,&buffer,nb);
   }

   jpeg_finish_decompress(&cinfo);                                               //  wrap-up
   jpeg_destroy_decompress(&cinfo);
   fclose(fid);
   zfree(row_pointers);
   row_pointers = 0;

   nc2 = 3;
   pxm = PXM_make(ww,hh,nc2);                                                    //  output PXM, no alpha channel
   if (! pxm) goto errret;
   
   pixelvert(pixels,pxm->pixels,ww,hh,nc1,nc2);                                  //  convert pixel data                 19.0
   
   PXM_audit(pxm);                                                               //  audit/repair                       20.0

   zfree(pixels);
   pixels = 0;

   f_load_bpc = 8;
   return pxm;

errret:
   if (errno) printz("error: %s \n",strerror(errno));
   printz("jpeg file error: %s \n",file);
   if (fid && fileno(fid) != -1) fclose(fid);
   if (row_pointers) zfree(row_pointers);
   if (pixels) zfree(pixels);
   return 0;
}


//  Write PXB pixmap to JPG file using JPG library.
//  quality is compression quality 0-100
//  returns 0 if OK, +N if error.

int PXB_JPG_save(PXB *pxb, cchar *file, int quality)
{
   struct jpeg_compress_struct      cinfo;
   struct libjpeg_error_mgr         jerr;

   FILE     *fid = 0;
   int      ww, hh, nc1, nc2, cc, row, nn;
   uint8    **row_pointers = 0;
   uint8    *pixels1, *pixels2 = 0;
  
   fid = fopen(file,"w");
   if (! fid) goto errret;

   cinfo.err = jpeg_std_error(&jerr.pub);                                        //  replace standard error handler
   jerr.pub.error_exit = libjpeg_error_exit;                                     //    which exits on error

   if (setjmp(jerr.setjmp_buffer)) {
      jpeg_destroy_compress(&cinfo);
      goto errret;
   }

   jpeg_create_compress(&cinfo);                                                 //  initz. for write
   jpeg_stdio_dest(&cinfo,fid);
   
   ww = pxb->ww;                                                                 //  image dimensions
   hh = pxb->hh;
   nc1 = pxb->nc;                                                                //  input channels (3/4)
   nc2 = 3;                                                                      //  output channels
   
   pixels1 = pxb->pixels;                                                        //  input pixels
   pixels2 = (uint8 *) zmalloc(ww * hh * nc2);                                   //  output pixels
   
   pixelvert(pixels1,pixels2,ww,hh,nc1,nc2);                                     //  convert pixel data                 19.0

   cinfo.image_width = ww;
   cinfo.image_height = hh;
   cinfo.input_components = nc2;
   cinfo.in_color_space = JCS_RGB;

   jpeg_set_defaults(&cinfo);                                                    //  default compression parameters
   jpeg_set_quality(&cinfo,quality,TRUE);                                        //  compression quality 0-100
   jpeg_start_compress(&cinfo,TRUE);
   
   cc = hh * sizeof(void *);                                                     //  allocate row pointers
   row_pointers = (uint8 **) zmalloc(cc);

   for (row = 0; row < hh; row++)                                                //  initz. row pointers
      row_pointers[row] = pixels2 + row * ww * nc2;
   
   nn = jpeg_write_scanlines(&cinfo,row_pointers,hh);                            //  write rows
   if (nn != hh) goto errret;

   jpeg_finish_compress(&cinfo);                                                 //  wrap-up
   jpeg_destroy_compress(&cinfo);
   fclose(fid);
   zfree(row_pointers);
   zfree(pixels2);
   row_pointers = 0;
   pixels2 = 0;

   return 0;

errret:
   if (errno) printz("error: %s \n",strerror(errno));
   printz("jpeg file error: %s \n",file);
   if (fid && fileno(fid) != -1) fclose(fid);
   if (row_pointers) zfree(row_pointers);
   if (pixels2) zfree(pixels2);
   return 3;
}


//  Write PXM pixmap to JPG file using JPG library.
//  quality is compression quality 0-100
//  returns 0 if OK, +N if error.

int PXM_JPG_save(PXM *pxm, cchar *file, int quality)
{
   struct jpeg_compress_struct      cinfo;
   struct libjpeg_error_mgr         jerr;

   FILE     *fid = 0;
   int      ww, hh, nc1, nc2, cc, row, nn;
   uint8    **row_pointers = 0;
   uint8    *pixels2 = 0;
   float    *pixels1;
   
   fid = fopen(file,"w");
   if (! fid) goto errret;

   cinfo.err = jpeg_std_error(&jerr.pub);                                        //  replace standard error handler
   jerr.pub.error_exit = libjpeg_error_exit;                                     //    which exits on error

   if (setjmp(jerr.setjmp_buffer)) {
      jpeg_destroy_compress(&cinfo);
      goto errret;
   }

   jpeg_create_compress(&cinfo);                                                 //  initz. for write
   jpeg_stdio_dest(&cinfo,fid);
   
   ww = pxm->ww;                                                                 //  image dimensions
   hh = pxm->hh;

   nc1 = pxm->nc;                                                                //  3 or 4 channels  RGB[A]
   nc2 = 3;

   pixels1 = pxm->pixels;                                                        //  input float *
   pixels2 = (uint8 *) zmalloc(ww * hh * nc2);                                   //  output uint8 *
   
   pixelvert(pixels1,pixels2,ww,hh,nc1,nc2);                                     //  convert pixel data                 19.0

   cinfo.image_width = ww;
   cinfo.image_height = hh;
   cinfo.input_components = nc2;                                                 //  RGB
   cinfo.in_color_space = JCS_RGB;

   jpeg_set_defaults(&cinfo);                                                    //  default compression parameters
   jpeg_set_quality(&cinfo,quality,TRUE);                                        //  compression quality 0-100
   jpeg_start_compress(&cinfo,TRUE);
   
   cc = hh * sizeof(void *);                                                     //  allocate row pointers
   row_pointers = (uint8 **) zmalloc(cc);

   for (row = 0; row < hh; row++)                                                //  initz. row pointers
      row_pointers[row] = pixels2 + row * ww * nc2;
   
   nn = jpeg_write_scanlines(&cinfo,row_pointers,hh);                            //  write rows
   if (nn != hh) goto errret;

   jpeg_finish_compress(&cinfo);                                                 //  wrap-up
   jpeg_destroy_compress(&cinfo);
   fclose(fid);

   zfree(row_pointers);
   zfree(pixels2);
   row_pointers = 0;
   pixels2 = 0;

   return 0;

errret:
   if (errno) printz("error: %s \n",strerror(errno));
   printz("jpeg file error: %s \n",file);
   if (fid && fileno(fid) != -1) fclose(fid);
   if (row_pointers) zfree(row_pointers);
   if (pixels2) zfree(pixels2);
   return 2;
}

/********************************************************************************/


#pragma GCC pop_options                                                          //  revert to normal optimization


/********************************************************************************
   TIFF file read and write functions
*********************************************************************************/

//  Intercept TIFF warning messages (many)

void tiffwarninghandler(cchar *module, cchar *format, va_list ap)
{
   return;                                                                       //  stop flood of crap
   char  message[200];
   vsnprintf(message,199,format,ap);
   printz("TIFF warning: %s %s \n",module,message);
   return;
}


//  Load PXB pixmap from TIFF file using TIFF library.
//  Native TIFF file bits/color >> f_load_bpc

PXB * TIFF_PXB_load(cchar *file)
{
   static int  ftf = 1;
   TIFF        *tiff;
   PXB         *pxb;
   char        *tiffbuff;
   uint16      bpc, nc1, nc2, pmi;
   int         err, ww, hh, rs1, rps, stb, nst;
   int         row, strip, cc;
   STATB       statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   if (ftf) {
      TIFFSetWarningHandler(tiffwarninghandler);                                 //  intercept TIFF warning messages
      ftf = 0;
   }

   tiff = TIFFOpen(file,"r");
   if (! tiff) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &ww);                                  //  width
   TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &hh);                                 //  height
   TIFFGetField(tiff, TIFFTAG_BITSPERSAMPLE, &bpc);                              //  bits per color, 1/8/16
   TIFFGetField(tiff, TIFFTAG_SAMPLESPERPIXEL, &nc1);                            //  no. channels (colors), 1/2/3/4
   TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rps);                               //  rows per strip
   TIFFGetField(tiff, TIFFTAG_PHOTOMETRIC, &pmi);                                //  1 = grayscale, 2 = RGB
   stb = TIFFStripSize(tiff);                                                    //  strip size
   nst = TIFFNumberOfStrips(tiff);                                               //  number of strips

   if (ww > wwhh_limit1 || hh > wwhh_limit1 || ww * hh > wwhh_limit2) {
      printz("image too big, %dx%d",ww,hh);
      TIFFClose(tiff);
      goto errret;
   }
   
   if (bpc != 8 && bpc != 16) {                                                  //  bits/color
      TIFFClose(tiff);
      return ANY_PXB_load(file);                                                 //  fallback to pixbuf loader
   }

   rs1 = ww * nc1;                                                               //  row stride
   if (bpc == 16) rs1 = rs1 * 2;

   tiffbuff = (char *) zmalloc(hh * rs1 + 1000000);                              //  tiff input buffer (crash prone)

   for (strip = 0; strip < nst; strip++)                                         //  read strips
   {
      row = strip * rps;                                                         //  1st row in strip
      cc = TIFFReadEncodedStrip(tiff,strip,tiffbuff+row*rs1,stb);
      if (cc > 0) continue;
      if (cc == 0) break;
      TIFFClose(tiff);
      zfree(tiffbuff);
      return ANY_PXB_load(file);                                                 //  fallback to pixbuf loader          19.0
   }

   TIFFClose(tiff);

   if (nc1 == 2 || nc1 == 4) nc2 = 4;                                            //  alpha channel present
   else nc2 = 3;
   
   pxb = PXB_make(ww,hh,nc2);                                                    //  create PXB
   if (! pxb) {
      printz("PXB_make() failure");
      goto errret;
   }

   if (bpc == 8) pixelvert((uint8 *) tiffbuff,pxb->pixels,ww,hh,nc1,nc2);        //  convert pixel data                 19.0
   if (bpc == 16) pixelvert((uint16 *) tiffbuff,pxb->pixels,ww,hh,nc1,nc2);

   zfree(tiffbuff);

   f_load_bpc = bpc;
   return pxb;

errret:
   return 0;
}


//  Load PXM pixmap from TIFF file using TIFF library.
//  Native TIFF file bits/color >> f_load_bpc

PXM * TIFF_PXM_load(cchar *file)
{
   static int  ftf = 1;
   TIFF        *tiff;
   PXM         *pxm;
   char        *tiffbuff;
   uint16      bpc, nc1, nc2, pmi;
   int         err, ww, hh, rs1, rps, stb, nst;
   int         row, strip, cc;
   STATB       statb;
   
   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   if (ftf) {
      TIFFSetWarningHandler(tiffwarninghandler);                                 //  intercept TIFF warning messages
      ftf = 0;
   }

   tiff = TIFFOpen(file,"r");
   if (! tiff) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   TIFFGetField(tiff, TIFFTAG_IMAGEWIDTH, &ww);                                  //  width
   TIFFGetField(tiff, TIFFTAG_IMAGELENGTH, &hh);                                 //  height
   TIFFGetField(tiff, TIFFTAG_BITSPERSAMPLE, &bpc);                              //  bits per color, 8/16
   TIFFGetField(tiff, TIFFTAG_SAMPLESPERPIXEL, &nc1);                            //  no. channels (colors), 1/2/3/4
   TIFFGetField(tiff, TIFFTAG_ROWSPERSTRIP, &rps);                               //  rows per strip
   TIFFGetField(tiff, TIFFTAG_SAMPLEFORMAT, &pmi);                               //  1 = grayscale, 2 = RGB
   stb = TIFFStripSize(tiff);                                                    //  strip size
   nst = TIFFNumberOfStrips(tiff);                                               //  number of strips

   if (ww > wwhh_limit1 || hh > wwhh_limit1 || ww * hh > wwhh_limit2) {
      printz("image too big, %dx%d",ww,hh);
      TIFFClose(tiff);
      goto errret;
   }

   if (bpc != 8 && bpc != 16) {                                                  //  bits/color
      TIFFClose(tiff);
      return ANY_PXM_load(file);                                                 //  fallback to pixbuf loader
   }

   rs1 = ww * nc1;                                                               //  row stride
   if (bpc == 16) rs1 = rs1 * 2;

   tiffbuff = (char *) zmalloc(hh * rs1 + 1000000);                              //  tiff input buffer (crash prone)
   
   for (strip = 0; strip < nst; strip++)                                         //  read strips
   {
      row = strip * rps;                                                         //  1st row in strip
      cc = TIFFReadEncodedStrip(tiff,strip,tiffbuff+row*rs1,stb);
      if (cc > 0) continue;
      if (cc == 0) break;
      TIFFClose(tiff);                                                           //  failed
      zfree(tiffbuff);
      return ANY_PXM_load(file);                                                 //  fallback to pixbuf loader          19.0
   }

   TIFFClose(tiff);

   if (nc1 == 2 || nc1 == 4) nc2 = 4;                                            //  alpha channel 
   else nc2 = 3;
   
   pxm = PXM_make(ww,hh,nc2);                                                    //  create output PXM
   if (! pxm) {
      TIFFClose(tiff);
      goto errret;
   }
   
   if (bpc == 8) pixelvert((uint8 *) tiffbuff,pxm->pixels,ww,hh,nc1,nc2);        //  convert pixel data                 19.0
   if (bpc == 16) pixelvert((uint16 *) tiffbuff,pxm->pixels,ww,hh,nc1,nc2);

   PXM_audit(pxm);                                                               //  audit/repair                       20.0

   zfree(tiffbuff);

   f_load_bpc = bpc;
   return pxm;

errret:
   return 0;
}


//  Write PXB pixmap to TIFF file using TIFF library.
//  TIFF file bpc is 8 or 16.
//  Returns 0 if OK, +N if error.

int PXB_TIFF_save(PXB *pxb, cchar *file, int bpc)
{
   static int  ftf = 1;
   TIFF        *tiff;
   char        *tiffbuff;
   int         tiffstat = 0;
   int         ww, hh, row;                                                      //  int not uint
   int         nc1, nc2, rs2, pm = 2, pc = 1;
   int         comp = tiff_comp_method;                                          //  20.0

   if (ftf) {
      TIFFSetWarningHandler(tiffwarninghandler);                                 //  intercept TIFF warning messages
      ftf = 0;
   }

   tiff = TIFFOpen(file,"w");
   if (! tiff) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   ww = pxb->ww;
   hh = pxb->hh;
   nc1 = pxb->nc;

   nc2 = nc1;

   rs2 = ww * nc2;                                                               //  output row stride
   if (bpc == 16) rs2 = rs2 * 2;
   
   tiffbuff = (char *) zmalloc(hh * rs2);

   TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, ww);
   TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, bpc);
   TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, nc2);
   TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, pm);
   TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, pc);
   TIFFSetField(tiff, TIFFTAG_COMPRESSION, comp);
   
   if (bpc == 8) pixelvert(pxb->pixels,(uint8 *) tiffbuff,ww,hh,nc1,nc2);
   if (bpc == 16) pixelvert(pxb->pixels,(uint16 *) tiffbuff,ww,hh,nc1,nc2);

   for (row = 0; row < hh; row++)
   {
      tiffstat = TIFFWriteScanline(tiff,tiffbuff+row*rs2,row,0);
      if (tiffstat != 1) break;
   }

   TIFFClose(tiff);
   zfree(tiffbuff);

   if (tiffstat == 1) return 0;

errret:
   return 2;
}


//  Write PXM pixmap to TIFF file using TIFF library.
//  TIFF file bpc is 8 or 16.
//  Returns 0 if OK, +N if error.

int PXM_TIFF_save(PXM *pxm, cchar *file, int bpc)
{
   static int  ftf = 1;
   TIFF        *tiff;
   char        *tiffbuff;
   int         tiffstat = 0;
   int         ww, hh, row;                                                      //  int not uint
   int         nc1, nc2, rs2, pm = 2, pc = 1;
   int         comp = tiff_comp_method;                                          //  20.0

   if (ftf) {
      TIFFSetWarningHandler(tiffwarninghandler);                                 //  intercept TIFF warning messages
      ftf = 0;
   }

   tiff = TIFFOpen(file,"w");
   if (! tiff) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   ww = pxm->ww;
   hh = pxm->hh;
   nc1 = pxm->nc;

   nc2 = nc1;

   rs2 = ww * nc2;                                                               //  output row stride
   if (bpc == 16) rs2 = rs2 * 2;
   
   tiffbuff = (char *) zmalloc(hh * rs2);

   TIFFSetField(tiff, TIFFTAG_IMAGEWIDTH, ww);
   TIFFSetField(tiff, TIFFTAG_BITSPERSAMPLE, bpc);
   TIFFSetField(tiff, TIFFTAG_SAMPLESPERPIXEL, nc2);
   TIFFSetField(tiff, TIFFTAG_PHOTOMETRIC, pm);
   TIFFSetField(tiff, TIFFTAG_PLANARCONFIG, pc);
   TIFFSetField(tiff, TIFFTAG_COMPRESSION, comp);
   
   if (bpc == 8) pixelvert(pxm->pixels,(uint8 *) tiffbuff,ww,hh,nc1,nc2);
   if (bpc == 16) pixelvert(pxm->pixels,(uint16 *) tiffbuff,ww,hh,nc1,nc2);

   for (row = 0; row < hh; row++)
   {
      tiffstat = TIFFWriteScanline(tiff,tiffbuff+row*rs2,row,0);
      if (tiffstat != 1) break;
   }

   TIFFClose(tiff);
   zfree(tiffbuff);

   if (tiffstat == 1) return 0;

errret:
   return 2;
}


/********************************************************************************
   PNG file read and write functions
*********************************************************************************/

//  Load PXB pixmap from PNG file using PNG library.
//  Native PNG file bits/color >> f_load_bpc

PXB * PNG_PXB_load(cchar *file)
{
   png_structp    pngstruct = 0;
   png_infop      pnginfo = 0;
   FILE           *fid = 0;
   int            err, ww, hh, bpc, nc1, nc2, rs1, row, colortype;
   uchar          *pngbuff, **pngrows;
   PXB            *pxb;
   STATB          statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   fid = fopen(file,"r");                                                        //  open file
   if (! fid) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   pngstruct = png_create_read_struct(PNG_LIBPNG_VER_STRING,0,0,0);              //  create png structs
   if (! pngstruct) goto errret;
   pnginfo = png_create_info_struct(pngstruct);
   if (! pnginfo) goto errret;
   if (setjmp(png_jmpbuf(pngstruct))) goto errret;                               //  setup error handling

   png_init_io(pngstruct,fid);                                                   //  read png file
   png_read_png(pngstruct,pnginfo,PNG_TRANSFORM_SWAP_ENDIAN,0);
   fclose(fid);

   ww = png_get_image_width(pngstruct,pnginfo);                                  //  width
   hh = png_get_image_height(pngstruct,pnginfo);                                 //  height
   bpc = png_get_bit_depth(pngstruct,pnginfo);                                   //  bits per color (channel)
   nc1 = png_get_channels(pngstruct,pnginfo);                                    //  no. channels
   colortype = png_get_color_type(pngstruct,pnginfo);                            //  color type
   pngrows = png_get_rows(pngstruct,pnginfo);                                    //  array of png row pointers

   if (colortype == PNG_COLOR_TYPE_GRAY || colortype == PNG_COLOR_TYPE_RGB)
      nc2 = 3;
   else if (colortype == PNG_COLOR_TYPE_GRAY_ALPHA || colortype == PNG_COLOR_TYPE_RGB_ALPHA)
      nc2 = 4;
   else {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      return ANY_PXB_load(file);                                                 //  fallback to pixbuf loader
   }

   if (ww > wwhh_limit1 || hh > wwhh_limit1 || ww * hh > wwhh_limit2) {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      printz("image too big, %dx%d",ww,hh);
      goto errret;
   }

   if (bpc != 8 && bpc != 16) {                                                  //  not 8 or 16 bits per channel
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      return ANY_PXB_load(file);                                                 //  use standard pixbuf loader
   }
   
   if (nc1 > 4) {                                                                //  not gray (+alpha) or RGB (+alpha)
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      return ANY_PXB_load(file);                                                 //  use standard pixbuf loader
   }

   pxb = PXB_make(ww,hh,nc2);                                                    //  create output PXB
   if (! pxb) {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      printz("PXB_make() failed");
      goto errret;
   }

   rs1 = ww * nc1;                                                               //  png input row stride
   if (bpc == 16) rs1 = rs1 * 2;

   pngbuff = (uchar *) zmalloc(hh * rs1);                                        //  png file input buffer
   
   for (row = 0; row < hh; row++)                                                //  get all png rows
      memcpy(pngbuff+row*rs1,pngrows[row],rs1);

   png_destroy_read_struct(&pngstruct,&pnginfo,0);                               //  release memory

   if (bpc == 8) pixelvert((uint8 *) pngbuff,pxb->pixels,ww,hh,nc1,nc2);         //  convert pixel data                 19.0
   if (bpc == 16) pixelvert((uint16 *) pngbuff,pxb->pixels,ww,hh,nc1,nc2);
   
   zfree(pngbuff);

   f_load_bpc = bpc;
   return pxb;

errret:
   if (fid && fileno(fid) != -1) fclose(fid);
   return 0;
}


//  Load PXM pixmap from PNG file using PNG library.
//  Native PNG file bits/color >> f_load_bpc

PXM * PNG_PXM_load(cchar *file)
{
   png_structp    pngstruct = 0;
   png_infop      pnginfo = 0;
   FILE           *fid = 0;
   int            err, ww, hh, bpc, nc1, nc2, rs1, row, colortype;
   uchar          *pngbuff, **pngrows;
   PXM            *pxm;
   STATB          statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   fid = fopen(file,"r");                                                        //  open file
   if (! fid) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   pngstruct = png_create_read_struct(PNG_LIBPNG_VER_STRING,0,0,0);              //  create png structs
   if (! pngstruct) goto errret;
   pnginfo = png_create_info_struct(pngstruct);
   if (! pnginfo) goto errret;
   if (setjmp(png_jmpbuf(pngstruct))) goto errret;                               //  setup error handling

   png_init_io(pngstruct,fid);                                                   //  read png file
   png_read_png(pngstruct,pnginfo,PNG_TRANSFORM_SWAP_ENDIAN,0);
   fclose(fid);

   ww = png_get_image_width(pngstruct,pnginfo);                                  //  width
   hh = png_get_image_height(pngstruct,pnginfo);                                 //  height
   bpc = png_get_bit_depth(pngstruct,pnginfo);                                   //  bits per color (channel)
   nc1 = png_get_channels(pngstruct,pnginfo);                                    //  no. channels
   colortype = png_get_color_type(pngstruct,pnginfo);                            //  color type
   pngrows = png_get_rows(pngstruct,pnginfo);                                    //  array of png row pointers

   if (colortype == PNG_COLOR_TYPE_GRAY || colortype == PNG_COLOR_TYPE_RGB)
      nc2 = 3;
   else if (colortype == PNG_COLOR_TYPE_GRAY_ALPHA || colortype == PNG_COLOR_TYPE_RGB_ALPHA)
      nc2 = 4;
   else {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      return ANY_PXM_load(file);                                                 //  fallback to pixbuf loader
   }

   if (ww > wwhh_limit1 || hh > wwhh_limit1 || ww * hh > wwhh_limit2) {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      printz("image too big, %dx%d",ww,hh);
      goto errret;
   }

   if (bpc != 8 && bpc != 16) {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      return ANY_PXM_load(file);                                                 //  use standard pixbuf loader
   }

   if (nc1 > 4) {                                                                //  not gray (+alpha) or RGB (+alpha)
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      return ANY_PXM_load(file);                                                 //  use standard pixbuf loader
   }
   
   pxm = PXM_make(ww,hh,nc2);                                                    //  create PXM 
   if (! pxm) {
      png_destroy_read_struct(&pngstruct,&pnginfo,0);
      goto errret;
   }

   rs1 = ww * nc1;                                                               //  png input row stride
   if (bpc == 16) rs1 = rs1 * 2;

   pngbuff = (uchar *) zmalloc(hh * rs1);                                        //  png file input buffer
   
   for (row = 0; row < hh; row++)                                                //  get all png rows
      memcpy(pngbuff+row*rs1,pngrows[row],rs1);

   png_destroy_read_struct(&pngstruct,&pnginfo,0);                               //  release memory

   if (bpc == 8) pixelvert((uint8 *) pngbuff,pxm->pixels,ww,hh,nc1,nc2);         //  convert pixel data                 19.0
   if (bpc == 16) pixelvert((uint16 *) pngbuff,pxm->pixels,ww,hh,nc1,nc2);
   
   PXM_audit(pxm);                                                               //  audit/repair                       20.0

   zfree(pngbuff);

   f_load_bpc = bpc;
   return pxm;

errret:
   if (fid && fileno(fid) != -1) fclose(fid);
   return 0;
}


//  Write PXB pixmap to PNG file using PNG library.
//  File bpc is 8 or 16.
//  returns 0 if OK, +N if error.

int PXB_PNG_save(PXB *pxb, cchar *file, int bpc)
{
   png_structp    pngstruct;
   png_infop      pnginfo;
   FILE           *fid = 0;
   uchar          *pngbuff, **pngrows;
   int            ww, hh, nc1, nc2, rs2, row;

   if (bpc != 8 && bpc != 16) {
      printz("PNG BPC not 8/16: %s",file);
      goto errret;
   }

   fid = fopen(file,"w");                                                        //  open output file
   if (! fid) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   ww = pxb->ww;
   hh = pxb->hh;
   nc1 = pxb->nc;
   nc2 = nc1;
   
   pngstruct = png_create_write_struct(PNG_LIBPNG_VER_STRING,0,0,0);             //  create png structs
   if (! pngstruct) goto errret;
   pnginfo = png_create_info_struct(pngstruct);
   if (! pnginfo) goto errret;
   if (setjmp(png_jmpbuf(pngstruct))) goto errret;                               //  setup error handling

   png_init_io(pngstruct,fid);                                                   //  initz. for writing file

   png_set_compression_level(pngstruct,png_comp_level);                          //  20.0

   if (nc1 == 4)
      png_set_IHDR(pngstruct,pnginfo,ww,hh,bpc,PNG_COLOR_TYPE_RGB_ALPHA,0,0,0);
   else
      png_set_IHDR(pngstruct,pnginfo,ww,hh,bpc,PNG_COLOR_TYPE_RGB,0,0,0);

   rs2 = ww * nc2;                                                               //  png row length
   if (bpc == 16) rs2 = rs2 * 2;
   
   pngbuff = (uchar *) zmalloc(hh * rs2);                                        //  allocate png file data

   pngrows = (uchar **) zmalloc(hh * sizeof(char *));                            //  allocate png row pointers
   png_set_rows(pngstruct,pnginfo,pngrows);

   for (row = 0; row < hh; row++)                                                //  set row pointers to row data
      pngrows[row] = pngbuff + row * rs2;

   if (bpc == 8) pixelvert(pxb->pixels,(uint8 *) pngbuff,ww,hh,nc1,nc2);         //  convert pixel data                 19.0
   if (bpc == 16) pixelvert(pxb->pixels,(uint16 *) pngbuff,ww,hh,nc1,nc2);

   png_write_png(pngstruct,pnginfo,PNG_TRANSFORM_SWAP_ENDIAN,0);                 //  write the file
   fclose(fid);

   png_destroy_write_struct(&pngstruct,&pnginfo);                                //  release memory

   zfree(pngbuff);
   zfree(pngrows);

   return 0;

errret:
   if (fid && fileno(fid) != -1) fclose(fid);
   return 2;
}


//  Write PXM pixmap to PNG file using PNG library.
//  File bpc is 8 or 16.
//  returns 0 if OK, +N if error.

int PXM_PNG_save(PXM *pxm, cchar *file, int bpc)
{
   png_structp    pngstruct;
   png_infop      pnginfo;
   FILE           *fid = 0;
   uchar          *pngbuff, **pngrows;
   int            ww, hh, nc1, nc2, rs2, row;

   if (bpc != 8 && bpc != 16) {
      printz("PNG BPC not 8/16: %s",file);
      goto errret;
   }

   fid = fopen(file,"w");                                                        //  open output file
   if (! fid) {
      printz("file error: %s %s \n",file,strerror(errno));
      goto errret;
   }

   ww = pxm->ww;
   hh = pxm->hh;
   nc1 = pxm->nc;
   nc2 = nc1;
   
   pngstruct = png_create_write_struct(PNG_LIBPNG_VER_STRING,0,0,0);             //  create png structs
   if (! pngstruct) goto errret;
   pnginfo = png_create_info_struct(pngstruct);
   if (! pnginfo) goto errret;
   if (setjmp(png_jmpbuf(pngstruct))) goto errret;                               //  setup error handling

   png_init_io(pngstruct,fid);                                                   //  initz. for writing file

   png_set_compression_level(pngstruct,png_comp_level);                          //  20.0

   if (nc1 == 4)
      png_set_IHDR(pngstruct,pnginfo,ww,hh,bpc,PNG_COLOR_TYPE_RGB_ALPHA,0,0,0);
   else
      png_set_IHDR(pngstruct,pnginfo,ww,hh,bpc,PNG_COLOR_TYPE_RGB,0,0,0);

   rs2 = ww * nc2;                                                               //  png row length
   if (bpc == 16) rs2 = rs2 * 2;
   
   pngbuff = (uchar *) zmalloc(hh * rs2);                                        //  allocate png file data

   pngrows = (uchar **) zmalloc(hh * sizeof(char *));                            //  allocate png row pointers
   png_set_rows(pngstruct,pnginfo,pngrows);

   for (row = 0; row < hh; row++)                                                //  set row pointers to row data
      pngrows[row] = pngbuff + row * rs2;

   if (bpc == 8) pixelvert(pxm->pixels,(uint8 *) pngbuff,ww,hh,nc1,nc2);         //  convert pixel data                 19.0
   if (bpc == 16) pixelvert(pxm->pixels,(uint16 *) pngbuff,ww,hh,nc1,nc2);

   png_write_png(pngstruct,pnginfo,PNG_TRANSFORM_SWAP_ENDIAN,0);                 //  write the file
   fclose(fid);

   png_destroy_write_struct(&pngstruct,&pnginfo);                                //  release memory

   zfree(pngbuff);
   zfree(pngrows);

   return 0;

errret:
   if (fid && fileno(fid) != -1) fclose(fid);
   return 2;
}


/********************************************************************************
   HEIC file read functions 
   (write functions are not implemented)
*********************************************************************************/

//  Load PXB pixmap from HEIC file using HEIC library (heif-convert).
//  Crutch: convert .heic file to .jpg and load .jpg file.
//  f_load_bpc = 8

PXB * HEIC_PXB_load(cchar *file, int size)                                       //  20.0
{
   int            err;
   char           *jpegfile = 0, *jpegfix = 0;
   char           *pp;
   PXB            *pxb;
   STATB          statb;
   
   if (! Fheif) {
      zmessageACK(Mwin,E2X("HEIF files not supported"));
      return 0;
   }

   err = stat(file,&statb);                                                      //  check file exists
   if (err || ! S_ISREG(statb.st_mode)) return 0;                                //  no print error
   
   jpegfile = zstrdup(file,8);                                                   //  file.heic  >>  file.jpg
   pp = strrchr(jpegfile,'.');
   if (! pp) goto errret;
   strcpy(pp,".jpg");
   
   err = shell_quiet("heif-convert -q %d \"%s\" \"%s\" >/dev/null ",             //  convert to .jpg
                                    jpeg_def_quality, file, jpegfile);
   err = stat(jpegfile,&statb);
   if (err) {
      jpegfix = zstrdup(jpegfile);                                               //  failed, may have multiple output files
      pp = strrchr(jpegfix,'.');                                                 //    file-1.jpg, file-2.jpg, etc.
      strcpy(pp,"-1.jpg");
      err = stat(jpegfix,&statb);
      if (err) goto errret;
      rename(jpegfix,jpegfile);                                                  //  if file-1.jpg, rename to file.jpg
      zfree(jpegfix);                                                            //  (file-2.jpg etc. remain) 
   }

   pxb = JPG_PXB_load(jpegfile,size);                                            //  make PXB from jpeg file

   remove(jpegfile);   
   zfree(jpegfile);
   return pxb;

errret:
   if (jpegfile) remove(jpegfile);
   if (jpegfile) zfree(jpegfile);
   if (jpegfix) zfree(jpegfix);
   return 0;
}


//  Load PXM pixmap from HEIC file using HEIC library (heif-convert).
//  Crutch: convert .heic file to .jpg and load .jpg file.
//  f_load_bpc = 8

PXM * HEIC_PXM_load(cchar *file)                                                 //  20.0
{
   int            err;
   char           *jpegfile = 0, *jpegfix = 0;
   char           *pp;
   PXM            *pxm;
   STATB          statb;

   if (! Fheif) {
      zmessageACK(Mwin,E2X("HEIF files not supported"));
      return 0;
   }

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) return 0;                                //  no print error
   
   jpegfile = zstrdup(file,8);
   pp = strrchr(jpegfile,'.');
   if (! pp) goto errret;
   strcpy(pp,".jpg");
   
   err = shell_quiet("heif-convert -q %d \"%s\" \"%s\" >/dev/null ",
                                    jpeg_def_quality, file, jpegfile);
   err = stat(jpegfile,&statb);
   if (err) {
      jpegfix = zstrdup(jpegfile);
      pp = strrchr(jpegfix,'.');
      strcpy(pp,"-1.jpg");
      err = stat(jpegfix,&statb);
      if (err) goto errret;
      rename(jpegfix,jpegfile);
      zfree(jpegfix);
   }

   pxm = JPG_PXM_load(jpegfile);

   remove(jpegfile);
   zfree(jpegfile);
   return pxm;

errret:
   if (jpegfile) remove(jpegfile);
   if (jpegfile) zfree(jpegfile);
   if (jpegfix) zfree(jpegfix);
   return 0;
}


int PXB_HEIC_save(PXB *pxb, cchar *file)
{
   zmessageACK(Mwin,"save to .heic file not implemented \n");
   return 0;
}


int PXM_HEIC_save(PXM *pxm, cchar *file)
{
   zmessageACK(Mwin,"save to .heic file not implemented \n");
   return 0;
}


/********************************************************************************
   JP2 file read functions 
   (write functions are not implemented)
*********************************************************************************/

//  Load PXB pixmap from JP2 file using jpeg2000 utility (opj_decompress).
//  Crutch: convert .jp2 file to .tif and load .tif file.
//  f_load_bpc = 8

PXB * JP2_PXB_load(cchar *file)                                                  //  20.0
{
   int         err, handle;
   char        *tiffile = 0;
   char        *pp;
   PXB         *pxb;
   STATB       statb;
   zdialog     *zdm = 0;
   
   if (! Fjp2) {
      zmessageACK(Mwin,E2X("JP2 files not supported"));
      return 0;
   }

   err = stat(file,&statb);                                                      //  check file exists
   if (err || ! S_ISREG(statb.st_mode)) return 0;                                //  no print error
   
   tiffile = zstrdup(file,8);                                                    //  file.jp2  >>  file.tif
   pp = strrchr(tiffile,'.');
   if (! pp) goto errret;
   strcpy(pp,".tif");

   if (statb.st_size > 2 * MEGA)                                                 //  may need a long time
      zdm = zmessage_post(Mwin,"20/20",0,"processing .jp2 file ...");

   handle = shell_asynch("opj_decompress -i \"%s\" -o \"%s\" >/dev/null",file,tiffile);
   while (true) {
      zmainsleep(0.1);
      err = shell_asynch_status(handle);
      if (err >= 0) break;
   }
   
   if (zdm) zdialog_free(zdm);
   if (err) goto errret;

   err = stat(tiffile,&statb);
   if (err) goto errret;

   pxb = TIFF_PXB_load(tiffile);                                                 //  make PXB from tif file

   remove(tiffile);
   zfree(tiffile);
   return pxb;

errret:
   if (tiffile) remove(tiffile);
   if (tiffile) zfree(tiffile);
   return 0;
}


//  Load PXM pixmap from JP2 file using jpeg2000 utility (opj_decompress).
//  Crutch: convert .jp2 file to .tif and load .tif file.
//  f_load_bpc = 8

PXM * JP2_PXM_load(cchar *file)                                                  //  20.0
{
   int         err;
   char        *tiffile = 0;
   char        *pp;
   PXM         *pxm;
   STATB       statb;
   zdialog     *zdm = 0;
   
   if (! Fjp2) {
      zmessageACK(Mwin,E2X("JP2 files not supported"));
      return 0;
   }

   err = stat(file,&statb);                                                      //  check file exists
   if (err || ! S_ISREG(statb.st_mode)) return 0;                                //  no print error
   
   tiffile = zstrdup(file,8);                                                    //  file.jp2  >>  file.tif
   pp = strrchr(tiffile,'.');
   if (! pp) goto errret;
   strcpy(pp,".tif");
   
   if (statb.st_size > 2 * MEGA)                                                 //  may need a long time
      zdm = zmessage_post(Mwin,"20/20",0,"processing .jp2 file ...");

   err = shell_quiet("opj_decompress -i \"%s\" -o \"%s\" >/dev/null ",           //  convert to .tif
                                                   file, tiffile);
   if (zdm) zdialog_free(zdm);
   if (err) goto errret;

   err = stat(tiffile,&statb);
   if (err) goto errret;

   pxm = TIFF_PXM_load(tiffile);                                                 //  make PXM from tif file

   remove(tiffile);
   zfree(tiffile);
   return pxm;

errret:
   if (tiffile) remove(tiffile);
   if (tiffile) zfree(tiffile);
   return 0;
}


int PXB_JP2_save(PXB *pxb, cchar *file)
{
   zmessageACK(Mwin,"save to .jp2 file not implemented \n");
   return 0;
}


int PXM_JP2_save(PXM *pxm, cchar *file)
{
   zmessageACK(Mwin,"save to .jp2 file not implemented \n");
   return 0;
}


/********************************************************************************
   Other file types read and write functions (via gdk_pixbuf library)
*********************************************************************************/

//  Load PXB pixmap from other file types using the pixbuf library.
//  bpc = 8.

PXB * ANY_PXB_load(cchar *file)
{
   GError      *gerror = 0;
   PXB         *pxb;
   PIXBUF      *pixbuf;
   int         err, ww, hh, px, py, nc1, nc2, ac, rs;
   uint8       *pixels1, *pix1;
   uint8       *pixels2, *pix2;
   STATB       statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   pixbuf = gdk_pixbuf_new_from_file(file,&gerror);
   if (! pixbuf) goto errret;

   ww = gdk_pixbuf_get_width(pixbuf);
   hh = gdk_pixbuf_get_height(pixbuf);
   nc1 = gdk_pixbuf_get_n_channels(pixbuf);
   ac = gdk_pixbuf_get_has_alpha(pixbuf);
   rs = gdk_pixbuf_get_rowstride(pixbuf);
   pixels1 = gdk_pixbuf_get_pixels(pixbuf);

   nc2 = 3 + ac;
   pxb = PXB_make(ww,hh,nc2);
   if (! pxb) {
      g_object_unref(pixbuf);
      goto errret;
   }

   pixels2 = pxb->pixels;

   for (py = 0; py < hh; py++)
   {
      pix1 = pixels1 + rs * py;
      pix2 = pixels2 + py * pxb->rs;

      if (nc1 >= 3)
      {
         for (px = 0; px < ww; px++)
         {
            memcpy(pix2,pix1,3);
            if (nc1 == 4) pix2[3] = pix1[3];                                     //  uint8 > unit8
            pix1 += nc1;
            pix2 += nc2;
         }
      }

      else
      {
         for (px = 0; px < ww; px++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            if (nc1 == 2) pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }

   g_object_unref(pixbuf);
   return pxb;

errret:
   printz("pixbuf read error: %s \n",file);
   if (gerror) printz("%s \n",gerror->message);
   return 0;
}


//  Load PXM pixmap from other file types using the pixbuf library.
//  bpc = 8.

PXM * ANY_PXM_load(cchar *file)
{
   GError      *gerror = 0;
   PXM         *pxm;
   PIXBUF      *pixbuf;
   int         err, ww, hh, px, py, nc1, nc2, ac, rs;
   uint8       *pixels1, *pix1;
   float       *pixels2, *pix2;
   STATB       statb;

   err = stat(file,&statb);
   if (err || ! S_ISREG(statb.st_mode)) goto errret;                             //  no print error                     19.13

   pixbuf = gdk_pixbuf_new_from_file(file,&gerror);
   if (! pixbuf) goto errret;

   ww = gdk_pixbuf_get_width(pixbuf);
   hh = gdk_pixbuf_get_height(pixbuf);
   nc1 = gdk_pixbuf_get_n_channels(pixbuf);
   ac = gdk_pixbuf_get_has_alpha(pixbuf);
   rs = gdk_pixbuf_get_rowstride(pixbuf);
   pixels1 = gdk_pixbuf_get_pixels(pixbuf);

   nc2 = 3 + ac;
   pxm = PXM_make(ww,hh,nc2);
   if (! pxm) {
      g_object_unref(pixbuf);
      goto errret;
   }
      
   pixels2 = pxm->pixels;

   for (py = 0; py < hh; py++)
   {
      pix1 = pixels1 + rs * py;
      pix2 = pixels2 + py * ww * nc2;

      if (nc1 >= 3)
      {
         for (px = 0; px < ww; px++)
         {
            pix2[0] = pix1[0];                                                   //  uint8 > float
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            if (nc1 == 4) pix2[3] = pix1[3]; 
            pix1 += nc1;
            pix2 += nc2;
         }
      }

      else
      {
         for (px = 0; px < ww; px++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            if (ac) pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }

   g_object_unref(pixbuf);

   PXM_audit(pxm);                                                               //  audit/repair                       20.0
   return pxm;

errret:
   printz("pixbuf read error: %s \n",file);
   if (gerror) printz("%s \n",gerror->message);
   return 0;
}


/********************************************************************************
   RAW file read functions. 
   There are no write functions.
*********************************************************************************/

int raw_match_thumb_color(cchar *rawfile, PXM *pxm);                             //  20.0
int raw_match_thumb_color(cchar *rawfile, PXB *pxb);


//  RAW file to PXB pixmap (pixbuf, 8-bit color)

PXB * RAW_PXB_load(cchar *rawfile, int autobright, int matchthumb)               //  20.0
{
   PXB   *pxb;

   if (Frawloader == 1 || ! Frawtherapeecli)
      pxb = RAW_PXB_load_dcraw(rawfile,autobright);
   else pxb = RAW_PXB_load_rawtherapee(rawfile,autobright);
   if (! pxb) return 0;

   if (matchthumb) raw_match_thumb_color(rawfile,pxb);
   return pxb;
}


//  RAW file to PXM pixmap (float color)

PXM * RAW_PXM_load(cchar *rawfile, int autobright, int matchthumb)               //  20.0
{
   PXM   *pxm;

   if (Frawloader == 1 || ! Frawtherapeecli)
      pxm = RAW_PXM_load_dcraw(rawfile,autobright);
   else pxm = RAW_PXM_load_rawtherapee(rawfile,autobright);
   if (! pxm) return 0;

   if (matchthumb) raw_match_thumb_color(rawfile,pxm);
   return pxm;
}


//  RAW file to PXB pixmap (pixbuf, 8-bit color)                                 //  20.0

PXB * RAW_PXB_load_dcraw(cchar *rawfile, int autobright)
{
   int      err;
   char     *tiffile, *pp;
   PXB      *pxb;
   
   cchar    *command;
   cchar    *command1 = "dcraw -w -T -q 0 \"%s\" ";                              //  tiff-8 output
   cchar    *command2 = "dcraw -w -T -q 0 -W \"%s\" ";
   
   if (autobright) command = command1;
   else command = command2;

   pp = zescape_quotes(rawfile);                                                 //  make tiff file from raw file
   err = shell_quiet(command,pp);
   zfree(pp);
   if (err) return 0;
   
   tiffile = zstrdup(rawfile,8);                                                 //  tiff file name = rawfile.tiff
   pp = strrchr(tiffile,'/');
   pp = strrchr(pp,'.');
   if (! pp) pp = tiffile + strlen(tiffile);
   strcpy(pp,".tiff");
   
   pxb = TIFF_PXB_load(tiffile);                                                 //  make raw PXB from tiff file
   remove(tiffile);
   zfree(tiffile);
   return pxb;
}


//  RAW file to PXB pixmap (pixbuf, 8-bit color)                                 //  20.0
//  use 1/2 size output file for faster thumbnail creation

PXB * RAW_PXB_load_dcraw_half(cchar *rawfile)
{
   int      err;
   char     *tiffile, *pp;
   PXB      *pxb;
   cchar    *command = "dcraw -w -T -q 0 \"%s\" ";
   
   pp = zescape_quotes(rawfile);                                                 //  make tiff file from raw file
   err = shell_quiet(command,pp);
   zfree(pp);
   if (err) return 0;
   
   tiffile = zstrdup(rawfile,8);                                                 //  tiff file name = rawfile.tiff
   pp = strrchr(tiffile,'/');
   pp = strrchr(pp,'.');
   if (! pp) pp = tiffile + strlen(tiffile);
   strcpy(pp,".tiff");
   
   pxb = TIFF_PXB_load(tiffile);                                                 //  make raw PXB from tiff file
   remove(tiffile);
   zfree(tiffile);
   return pxb;
}


//  RAW file to PXM pixmap (float color)                                         //  20.0

PXM * RAW_PXM_load_dcraw(cchar *rawfile, int autobright)
{
   int      err;
   char     *tiffile, *pp;
   PXM      *pxm;
   cchar    *command;
   cchar    *command1 = "dcraw -w -T -6 -q 0 \"%s\" ";                           //  tiff-16 output
   cchar    *command2 = "dcraw -w -T -6 -q 0 -W \"%s\" ";
   
   if (autobright) command = command1;
   else command = command2;

   pp = zescape_quotes(rawfile);                                                 //  make tiff file from raw file
   err = shell_quiet(command,pp);
   zfree(pp);
   if (err) return 0;
   
   tiffile = zstrdup(rawfile,8);                                                 //  tiff file name = rawfile.tiff
   pp = strrchr(tiffile,'/');
   pp = strrchr(pp,'.');
   if (! pp) pp = tiffile + strlen(tiffile);
   strcpy(pp,".tiff");
   
   pxm = TIFF_PXM_load(tiffile);                                                 //  make raw PXM from tiff file
   remove(tiffile);
   zfree(tiffile);
   return pxm;
}


//  RAW file to PXB pixmap (pixbuf, 8-bit color)

PXB * RAW_PXB_load_rawtherapee(cchar *rawfile, int autobright)                   //  20.0
{
   int      err;
   PXB      *pxb = 0;
   char     *pp, *tiffile;
   cchar    *command;
   cchar    *command1 = "rawtherapee-cli -q -d -t -b16 -Y -c \"%s\" >/dev/null";
   cchar    *command2 = "rawtherapee-cli -q -t -b16 -Y -c \"%s\" >/dev/null";
   
   Ffuncbusy++;
   zmainsleep(0.1);

   if (autobright) command = command1;
   else command = command2;
   
   pp = zescape_quotes(rawfile);                                                 //  use rawtherapee
   err = shell_quiet(command,pp);                                                //  convert to rawfile.tif
   zfree(pp);
   if (err) {
      printz("RAW conversion failed: %s \n",rawfile);
      Ffuncbusy--;
      return 0;
   }

   tiffile = raw_to_tiff(rawfile);                                               //  rawfile.tif
   pxb = TIFF_PXB_load(tiffile);

   remove(tiffile);
   zfree(tiffile);
   Ffuncbusy--;
   return pxb;
}


//  RAW file to PXM pixmap (float color)

PXM * RAW_PXM_load_rawtherapee(cchar *rawfile, int autobright)                   //  20.0
{
   int      err;
   PXM      *pxm = 0;
   char     *pp, *tiffile;
   cchar    *command;
   cchar    *command1 = "rawtherapee-cli -q -d -t -b16 -Y -c \"%s\" >/dev/null";
   cchar    *command2 = "rawtherapee-cli -q -t -b16 -Y -c \"%s\" >/dev/null";
   
   Ffuncbusy++;
   zmainsleep(0.1);

   if (autobright) command = command1;
   else command = command2;

   pp = zescape_quotes(rawfile);                                                 //  use rawtherapee
   err = shell_quiet(command,pp);                                                //  convert to rawfile.tif
   zfree(pp);
   if (err) {
      printz("RAW conversion failed: %s \n",rawfile);
      Ffuncbusy--;
      return 0;
   }

   tiffile = raw_to_tiff(rawfile);                                               //  rawfile.tif
   pxm = TIFF_PXM_load(tiffile);

   remove(tiffile);
   zfree(tiffile);
   Ffuncbusy--;
   return pxm;
}


//  Use the RAW file embedded thumbnail as a template for RAW image color balance.
//  Input PXM RGB values are revised to match thumbnail histogram. 
//  returns: 0 = OK, +N = error (PXM not changed).

int raw_match_thumb_color(cchar *rawfile, PXM *Rpxm)
{
   PXB      *Tpxb;
   char     *thumbfile, *pp;
   int      ii, jj, kk, err, rgb, step;
   int      Tpix, Rpix, Tpix3[3], Rpix3[3];
   int      Tdist[3][1024], Rdist[3][1024], Rval[3][1024];
   int      Rsum, Tsum, Rbin, Tbin;
   uint8    *Tpixel;
   float    *Rpixel;
   STATB    statb;

   pp = zescape_quotes(rawfile);                                                 //  get embedded thumbnail file
   err = shell_quiet("dcraw -e \"%s\" ",pp);                                     //    from raw file
   zfree(pp);
   if (err) return 1;
   
   thumbfile = zstrdup(rawfile,12);                                              //  rawfile.thumb.jpg
   pp = strrchr(thumbfile,'/');
   pp = strrchr(pp,'.');
   if (! pp) pp = thumbfile + strlen(thumbfile);
   strcpy(pp,".thumb.jpg");
   
   err = stat(thumbfile,&statb);
   if (! err) Tpxb = JPG_PXB_load(thumbfile);                                    //  make thumb PXB
   else {
      strcpy(pp+6,".ppm");
      Tpxb = ANY_PXB_load(thumbfile);
   }   
   
   remove(thumbfile);
   zfree(thumbfile);
   if (! Tpxb) return 2;                                                         //  fail

   Tpix = Tpxb->ww * Tpxb->hh;                                                   //  total thumb PXB pixels
   Rpix = Rpxm->ww * Rpxm->hh;                                                   //  total raw PXM pixels

   memset(Tdist,0,3*1024*sizeof(int));                                           //  clear thumb distribution
   
   for (ii = 0; ii < Tpix; ii++)                                                 //  loop thumb pixels
   for (rgb = 0; rgb < 3; rgb++)
   {
      Tpixel = Tpxb->pixels + ii * 3;                                            //  build thumb brightness distribution
      jj = 4 * Tpixel[rgb];                                                      //    for each RGB channel
      Tdist[rgb][jj]++;
      Tdist[rgb][jj+1] = Tdist[rgb][jj+2] = Tdist[rgb][jj+3] = Tdist[rgb][jj];   //  duplicate 4x for 1024 bins
   }
   
   for (ii = 0; ii < 255; ii++)                                                  //  ramp each set of 4 duplicates
   for (rgb = 0; rgb < 3; rgb++)                                                 //     10 10 10 10   20 20 20 20  40 ...
   {                                                                             //  >> 10 12 15 17   20 25 30 35  40 ...
      jj = 4 * ii;
      step = Tdist[rgb][jj+4] - Tdist[rgb][jj];
      for (kk = 1; kk < 4; kk++)
         Tdist[rgb][jj+kk] += kk * step / 4;
   }
   
   memset(Rdist,0,3*1024*sizeof(int));                                           //  clear raw distribution
   
   for (ii = 0; ii < Rpix; ii++)                                                 //  loop raw pixels
   for (rgb = 0; rgb < 3; rgb++)
   {
      Rpixel = Rpxm->pixels + ii * 3;                                            //  build raw brightness distribution
      jj = 4 * Rpixel[rgb];                                                      //  0.0 - 255.99  >>  bins 0 - 1023
      if (jj > 1023) jj = 1023;
      Rdist[rgb][jj]++;
   }

   for (rgb = 0; rgb < 3; rgb++)
      Tpix3[rgb] = Rpix3[rgb] = 0;
      
   for (ii = 0; ii < 1024; ii++)                                                 //  get distribution totals
   for (rgb = 0; rgb < 3; rgb++)
   {
      Tpix3[rgb] += Tdist[rgb][ii];
      Rpix3[rgb] += Rdist[rgb][ii];
   }

   for (ii = 0; ii < 1024; ii++)                                                 //  make Tdist totals = Rdist totals
   for (rgb = 0; rgb < 3; rgb++)
      Tdist[rgb][ii] *= 1.0 * Rpix3[rgb] / Tpix3[rgb];
 
   for (rgb = 0; rgb < 3; rgb++)                                                 //  low to high
   {
      for (Rsum = 0, Rbin = 0; Rbin < 1024; Rbin++)
      {
         Rsum += Rdist[rgb][Rbin];

         for (Tsum = 0, Tbin = 0; Tbin < 1024; Tbin++)
         {
            Tsum += Tdist[rgb][Tbin];
            if (Tsum > Rsum) break;
         }

         if (Tbin == 256) Tbin = 255;
         Rval[rgb][Rbin] = Tbin;
      }
   }

   for (rgb = 0; rgb < 3; rgb++)                                                 //  high to low
   {
      for (Rsum = 0, Rbin = 1023; Rbin >= 0; Rbin--)
      {
         Rsum += Rdist[rgb][Rbin];

         for (Tsum = 0, Tbin = 1023; Tbin >= 0; Tbin--)
         {
            Tsum += Tdist[rgb][Tbin];
            if (Tsum > Rsum) break;
         }

         if (Tbin < 0) Tbin = 0;
         Rval[rgb][Rbin] = 0.5 * (Rval[rgb][Rbin] + Tbin);                       //  average two distributions
      }
   }
   
   for (ii = 0; ii < Rpix; ii++)                                                 //  loop raw pixels
   {
      Rpixel = Rpxm->pixels + ii * 3;

      for (rgb = 0; rgb < 3; rgb++)                                              //  loop RGB color
      {
         Rbin = 4 * Rpixel[rgb];                                                 //  old brightness
         Rpixel[rgb] = 0.25 * Rval[rgb][Rbin];                                   //  set new brightness
      }
   }

   return 0;
}


//  PXB version of above.

int raw_match_thumb_color(cchar *rawfile, PXB *Rpxb)
{
   PXB      *Tpxb;
   char     *thumbfile, *pp;
   int      ii, jj, kk, err, rgb, step;
   int      Tpix, Rpix, Tpix3[3], Rpix3[3];
   int      Tdist[3][1024], Rdist[3][1024], Rval[3][1024];
   int      Rsum, Tsum, Rbin, Tbin;
   uint8    *Tpixel, *Rpixel;
   STATB    statb;

   pp = zescape_quotes(rawfile);                                                 //  get embedded thumbnail file
   err = shell_quiet("dcraw -e \"%s\" ",pp);                                     //    from raw file
   zfree(pp);
   if (err) return 1;
   
   thumbfile = zstrdup(rawfile,12);                                              //  rawfile.thumb.jpg
   pp = strrchr(thumbfile,'/');
   pp = strrchr(pp,'.');
   if (! pp) pp = thumbfile + strlen(thumbfile);
   strcpy(pp,".thumb.jpg");
   
   err = stat(thumbfile,&statb);
   if (! err) Tpxb = JPG_PXB_load(thumbfile);                                    //  make thumb PXB
   else {
      strcpy(pp+6,".ppm");
      Tpxb = ANY_PXB_load(thumbfile);
   }   
   
   remove(thumbfile);
   zfree(thumbfile);
   if (! Tpxb) return 2;                                                         //  fail

   Tpix = Tpxb->ww * Tpxb->hh;                                                   //  total thumb PXB pixels
   Rpix = Rpxb->ww * Rpxb->hh;                                                   //  total raw PXB pixels

   memset(Tdist,0,3*1024*sizeof(int));                                           //  clear thumb distribution
   
   for (ii = 0; ii < Tpix; ii++)                                                 //  loop thumb pixels
   for (rgb = 0; rgb < 3; rgb++)
   {
      Tpixel = Tpxb->pixels + ii * 3;                                            //  build thumb brightness distribution
      jj = 4 * Tpixel[rgb];                                                      //    for each RGB channel
      Tdist[rgb][jj]++;
      Tdist[rgb][jj+1] = Tdist[rgb][jj+2] = Tdist[rgb][jj+3] = Tdist[rgb][jj];   //  duplicate 4x for 1024 bins
   }
   
   for (ii = 0; ii < 255; ii++)                                                  //  ramp each set of 4 duplicates
   for (rgb = 0; rgb < 3; rgb++)                                                 //     10 10 10 10   20 20 20 20  40 ...
   {                                                                             //  >> 10 12 15 17   20 25 30 35  40 ...
      jj = 4 * ii;
      step = Tdist[rgb][jj+4] - Tdist[rgb][jj];
      for (kk = 1; kk < 4; kk++)
         Tdist[rgb][jj+kk] += kk * step / 4;
   }
   
   memset(Rdist,0,3*1024*sizeof(int));                                           //  clear raw distribution
   
   for (ii = 0; ii < Rpix; ii++)                                                 //  loop raw pixels
   for (rgb = 0; rgb < 3; rgb++)
   {
      Rpixel = Rpxb->pixels + ii * 3;                                            //  build raw brightness distribution
      jj = 4 * Rpixel[rgb];                                                      //  0.0 - 255.99  >>  bins 0 - 1023
      if (jj > 1023) jj = 1023;
      Rdist[rgb][jj]++;
   }

   for (rgb = 0; rgb < 3; rgb++)
      Tpix3[rgb] = Rpix3[rgb] = 0;
      
   for (ii = 0; ii < 1024; ii++)                                                 //  get distribution totals
   for (rgb = 0; rgb < 3; rgb++)
   {
      Tpix3[rgb] += Tdist[rgb][ii];
      Rpix3[rgb] += Rdist[rgb][ii];
   }

   for (ii = 0; ii < 1024; ii++)                                                 //  make Tdist totals = Rdist totals
   for (rgb = 0; rgb < 3; rgb++)
      Tdist[rgb][ii] *= 1.0 * Rpix3[rgb] / Tpix3[rgb];
 
   for (rgb = 0; rgb < 3; rgb++)                                                 //  low to high
   {
      for (Rsum = 0, Rbin = 0; Rbin < 1024; Rbin++)
      {
         Rsum += Rdist[rgb][Rbin];

         for (Tsum = 0, Tbin = 0; Tbin < 1024; Tbin++)
         {
            Tsum += Tdist[rgb][Tbin];
            if (Tsum > Rsum) break;
         }

         if (Tbin == 256) Tbin = 255;
         Rval[rgb][Rbin] = Tbin;
      }
   }

   for (rgb = 0; rgb < 3; rgb++)                                                 //  high to low
   {
      for (Rsum = 0, Rbin = 1023; Rbin >= 0; Rbin--)
      {
         Rsum += Rdist[rgb][Rbin];

         for (Tsum = 0, Tbin = 1023; Tbin >= 0; Tbin--)
         {
            Tsum += Tdist[rgb][Tbin];
            if (Tsum > Rsum) break;
         }

         if (Tbin < 0) Tbin = 0;
         Rval[rgb][Rbin] = 0.5 * (Rval[rgb][Rbin] + Tbin);                       //  average two distributions
      }
   }
   
   for (ii = 0; ii < Rpix; ii++)                                                 //  loop raw pixels
   {
      Rpixel = Rpxb->pixels + ii * 3;

      for (rgb = 0; rgb < 3; rgb++)                                              //  loop RGB color
      {
         Rbin = 4 * Rpixel[rgb];                                                 //  old brightness
         Rpixel[rgb] = 0.25 * Rval[rgb][Rbin];                                   //  set new brightness
      }
   }

   return 0;
}


/********************************************************************************

   pixel format conversion functions                                             19.0
   
   input pixel buffer:  uint8/uint16/float with 1/2/3/4 channels
   output pixel buffer: uint8/uint16/float with 3/4 channels
   
   row stride is assumed to have no padding at row ends and no
   shortening of the last row (not compatible with GDK pixbufs)

*********************************************************************************/

//  copy from uint8 buffer to uint8 buffer

void pixelvert(uint8 *buff1, uint8 *buff2, int ww, int hh, int nc1, int nc2)
{
   uint8    *pix1;
   uint8    *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memset(pix2,pix1[0],3);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memset(pix2,pix1[0],3);
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memset(pix2,pix1[0],3);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memset(pix2,pix1[0],3);
            pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,3);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,3);
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,3);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,4);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from uint8 buffer to uint16 buffer

void pixelvert(uint8 *buff1, uint16 *buff2, int ww, int hh, int nc1, int nc2)
{
   uint8    *pix1;
   uint16   *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix2[3] = 255 * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix2[3] = pix1[1] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix2[3] = 255 * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix2[3] = pix1[3] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from uint8 buffer to float buffer

void pixelvert(uint8 *buff1, float *buff2, int ww, int hh, int nc1, int nc2)
{
   uint8    *pix1;
   float    *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix2[3] = pix1[3];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from uint16 buffer to uint8 buffer

void pixelvert(uint16 *buff1, uint8 *buff2, int ww, int hh, int nc1, int nc2)
{
   uint16   *pix1;
   uint8    *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] >> 8;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] >> 8;
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] >> 8;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] >> 8;
            pix2[3] = pix1[1] >> 8;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] >> 8;
            pix2[1] = pix1[1] >> 8;
            pix2[2] = pix1[2] >> 8;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] >> 8;
            pix2[1] = pix1[1] >> 8;
            pix2[2] = pix1[2] >> 8;
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] >> 8;
            pix2[1] = pix1[1] >> 8;
            pix2[2] = pix1[2] >> 8;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] >> 8;
            pix2[1] = pix1[1] >> 8;
            pix2[2] = pix1[2] >> 8;
            pix2[3] = pix1[3] >> 8;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from uint16 buffer to uint16 buffer

void pixelvert(uint16 *buff1, uint16 *buff2, int ww, int hh, int nc1, int nc2)
{
   uint16   *pix1;
   uint16   *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,6);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,6);
            pix2[3] = 255 * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,6);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2,pix1,8);
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from uint16 buffer to float buffer

void pixelvert(uint16 *buff1, float *buff2, int ww, int hh, int nc1, int nc2)
{
   uint16   *pix1;
   float    *pix2;
   float    f256 = 1.0 / 256.0;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * f256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * f256;
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * f256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * f256;
            pix2[3] = pix1[1] * f256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * f256;
            pix2[1] = pix1[1] * f256;
            pix2[2] = pix1[2] * f256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * f256;
            pix2[1] = pix1[1] * f256;
            pix2[2] = pix1[2] * f256;
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * f256;
            pix2[1] = pix1[1] * f256;
            pix2[2] = pix1[2] * f256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * f256;
            pix2[1] = pix1[1] * f256;
            pix2[2] = pix1[2] * f256;
            pix2[3] = pix1[3] * f256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from float buffer to uint8 buffer

void pixelvert(float *buff1, uint8 *buff2, int ww, int hh, int nc1, int nc2)
{
   float    *pix1;
   uint8    *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0];
            pix2[1] = pix1[1];
            pix2[2] = pix1[2];
            pix2[3] = pix1[3];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from float buffer to uint16 buffer

void pixelvert(float *buff1, uint16 *buff2, int ww, int hh, int nc1, int nc2)
{
   float    *pix1;
   uint16   *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix2[3] = 255 * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0] * 256;
            pix2[3] = pix1[1] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix2[3] = 255 * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix1[0] * 256;
            pix2[1] = pix1[1] * 256;
            pix2[2] = pix1[2] * 256;
            pix2[3] = pix1[3] * 256;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


//  copy from float buffer to float buffer

void pixelvert(float *buff1, float *buff2, int ww, int hh, int nc1, int nc2)
{
   float    *pix1;
   float    *pix2;
   int      rs1 = ww * nc1, rs2 = ww * nc2;
   int      row, col;
   
   for (row = 0; row < hh; row++)
   {
      pix1 = buff1 + row * rs1;
      pix2 = buff2 + row * rs2;
      
      if (nc1 == 1 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 1 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 2 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            pix2[0] = pix2[1] = pix2[2] = pix1[0];
            pix2[3] = pix1[1];
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2, pix1, 3 * sizeof(float));
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 3 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2, pix1, 3 * sizeof(float));
            pix2[3] = 255;
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 3)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2, pix1, 3 * sizeof(float));
            pix1 += nc1;
            pix2 += nc2;
         }
      }
      
      if (nc1 == 4 && nc2 == 4)
      {
         for (col = 0; col < ww; col++)
         {
            memcpy(pix2, pix1, 4 * sizeof(float));
            pix1 += nc1;
            pix2 += nc2;
         }
      }
   }
   
   return;
}


