about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2022-06-08 20:27:23 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2022-06-08 20:27:23 +0000
commite865b4cd3c9092f8d8adfadd3a9c9a9843d507a2 (patch)
tree75ce815091c5fe9833807f55dff9c1486b0c11b1
parent88891c17336b2f7d6ea3f720eaaac40940a57d1f (diff)
downloadnetpbm-mirror-e865b4cd3c9092f8d8adfadd3a9c9a9843d507a2.tar.gz
netpbm-mirror-e865b4cd3c9092f8d8adfadd3a9c9a9843d507a2.tar.xz
netpbm-mirror-e865b4cd3c9092f8d8adfadd3a9c9a9843d507a2.zip
Accept input filename argument; don't use intermediate buffer or generic QOI module
git-svn-id: http://svn.code.sf.net/p/netpbm/code/trunk@4352 9d0c8265-081b-0410-96cb-a4ca84ce46f8
-rw-r--r--converter/other/Makefile7
-rw-r--r--converter/other/pamtoqoi.c493
-rw-r--r--converter/other/qoi.c365
-rw-r--r--converter/other/qoi.h269
-rw-r--r--converter/other/qoitopam.c309
5 files changed, 677 insertions, 766 deletions
diff --git a/converter/other/Makefile b/converter/other/Makefile
index b1f8461a..3b3b6aa0 100644
--- a/converter/other/Makefile
+++ b/converter/other/Makefile
@@ -169,7 +169,7 @@ BINARIES = $(PORTBINARIES)
 
 MERGEBINARIES = $(BINARIES)
 
-EXTRA_OBJECTS = exif.o rast.o ipdb.o srf.o qoi.o
+EXTRA_OBJECTS = exif.o rast.o ipdb.o srf.o
 ifeq ($(HAVE_PNGLIB),Y)
   EXTRA_OBJECTS += pngtxt.o
   EXTRA_OBJECTS += pngx.o
@@ -239,11 +239,6 @@ jpegtopnm: jpegdatasource.o exif.o
 jpegtopnm: ADDL_OBJECTS = jpegdatasource.o exif.o
 jpegtopnm: LDFLAGS_TARGET = $(shell $(LIBOPT) $(LIBOPTR) $(JPEGLIB))
 
-pamtoqoi: pamtoqoi.o qoi.o
-pamtoqoi: ADDL_OBJECTS = qoi.o
-qoitopam: pamtoqoi.o qoi.o
-qoitopam: ADDL_OBJECTS = qoi.o
-
 srftopam pamtosrf: srf.o
 srftopam pamtosrf: ADDL_OBJECTS = srf.o
 
diff --git a/converter/other/pamtoqoi.c b/converter/other/pamtoqoi.c
index 6bad6a90..638efa3a 100644
--- a/converter/other/pamtoqoi.c
+++ b/converter/other/pamtoqoi.c
@@ -1,184 +1,439 @@
 /*
- * This file is part of Netpbm (http://netpbm.sourceforge.org).
- * Copyright (c) 2022 cancername.
- *
- * 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, version 3.
- *
- * 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.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
+  pamtoqoi -  Converts PAM to a QOI - The "Quite OK Image" format file
+
+  This program is part of Netpbm.
+
+  ---------------------------------------------------------------------
+
+  QOI - The "Quite OK Image" format for fast, lossless image compression
+
+  Encoder by Dominic Szablewski - https://phoboslab.org
+
+  -- LICENSE: The MIT License(MIT)
+
+  Copyright(c) 2021 Dominic Szablewski
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files(the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions :
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+  For more information on the format visit: https//qoiformat.org/ .
+
+  Modifications for Netpbm read routines by Akira F. Urushibata.
+
+*/
+
 #include <string.h>
 #include <assert.h>
 #include "pam.h"
 #include "nstring.h"
+#include "mallocvar.h"
+#include "shhopt.h"
+
 #include "qoi.h"
 
-#define QOI_MAXVAL 0xFF
 
-/* tuple row to qoi row */
-typedef void Trqr(const tuplen *  const tuplerown,
-                  unsigned int    const width,
-                  unsigned char * const qoiRow);
 
-static Trqr trqrRgb;
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* '-' if stdin */
+};
 
-static void
-trqrRgb(const tuplen *  const tuplerown,
-        unsigned int    const cols,
-        unsigned char * const qoiRow) {
 
-    size_t qoiRowCursor;
-    unsigned int col;
 
-    for (col = 0, qoiRowCursor = 0; col < cols; ++col) {
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_RED_PLANE] * QOI_MAXVAL;
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_GRN_PLANE] * QOI_MAXVAL;
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_BLU_PLANE] * QOI_MAXVAL;
-    }
+static void
+parseCommandLine(int                  argc,
+                 const char **        argv,
+                 struct CmdlineInfo * cmdlineP ) {
+/*----------------------------------------------------------------------------
+   Parse program command line described in Unix standard form by argc
+   and argv.  Return the information in the options as *cmdlineP.
+
+   If command line is internally inconsistent (invalid options, etc.),
+   issue error message to stderr and abort program.
+
+   Note that the strings we return are stored in the storage that
+   was passed to us as the argv array.  We also trash *argv.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    MALLOCARRAY(option_def, 100);
+
+    OPTENTINIT;
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else
+        pm_error("Program takes at most one argument: input file name.  "
+            "you specified %d", argc-1);
 }
 
 
 
-static Trqr trqrRgba;
-
 static void
-trqrRgba(const tuplen *  const tuplerown,
-         unsigned int    const cols,
-         unsigned char * const qoiRow) {
-
-    size_t qoiRowCursor;
-    unsigned int col;
-
-    for (col = 0, qoiRowCursor = 0; col < cols; ++col) {
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_RED_PLANE] * QOI_MAXVAL;
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_GRN_PLANE] * QOI_MAXVAL;
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_BLU_PLANE] * QOI_MAXVAL;
-        qoiRow[qoiRowCursor++] = tuplerown[col][PAM_TRN_PLANE] * QOI_MAXVAL;
-    }
+encodeQoiHeader(qoi_Desc const qoiDesc) {
+
+    assert (QOI_MAGIC_SIZE + 4 + 4 + 1 + 1 == QOI_HEADER_SIZE);
+
+    fwrite (qoi_magic, QOI_MAGIC_SIZE, 1, stdout);
+    pm_writebiglongu(stdout, qoiDesc.width);
+    pm_writebiglongu(stdout, qoiDesc.height);
+    putchar(qoiDesc.channelCt);
+    putchar(qoiDesc.colorspace);
+
 }
 
+enum Tupletype {BW, BWAlpha, GRAY, GRAYAlpha, RGB, RGBAlpha,
+                GRAY255, GRAY255Alpha, RGB255, RGB255Alpha};
 
 
-static Trqr trqrGray;
 
 static void
-trqrGray(const tuplen *  const tuplerown,
-         unsigned int    const cols,
-         unsigned char * const qoiRow) {
+createSampleMap(sample    const oldMaxval,
+                sample ** const sampleMapP) {
 
-    size_t qoiRowCursor;
-    unsigned int col;
+    unsigned int i;
+    sample * sampleMap;
+    sample   const newMaxval = 255;
 
-    for (col = 0, qoiRowCursor = 0; col < cols; ++col) {
-        unsigned char const qoiSample = tuplerown[col][0] * QOI_MAXVAL;
+    MALLOCARRAY_NOFAIL(sampleMap, oldMaxval+1);
 
-        qoiRow[qoiRowCursor++] = qoiSample;
-        qoiRow[qoiRowCursor++] = qoiSample;
-        qoiRow[qoiRowCursor++] = qoiSample;
-    }
+    for (i = 0; i <= oldMaxval; ++i)
+        sampleMap[i] = ROUNDDIV(i * newMaxval, oldMaxval);
+
+    *sampleMapP = sampleMap;
 }
 
 
 
-static Trqr trqrGrayAlpha;
+static enum Tupletype
+tupleTypeFmPam(const char * const pamTupleType,
+               sample       const maxval) {
+
+    enum Tupletype retval;
+
+    if (streq(pamTupleType, PAM_PBM_TUPLETYPE)) {
+        if (maxval !=1)
+            pm_error("Invalid maxval (%lu) for tuple type '%s'.",
+                     maxval, pamTupleType);
+        else
+            retval =  BW;
+    } else if (streq(pamTupleType, PAM_PBM_ALPHA_TUPLETYPE)) {
+      if (maxval !=1)
+          pm_error("Invalid maxval (%lu) for tuple type '%s'.",
+                    maxval, pamTupleType);
+      else
+          retval =  BWAlpha;
+    } else if (maxval == 255) {
+        if (streq(pamTupleType, PAM_PPM_TUPLETYPE))
+            retval =  RGB255;
+        else if(streq(pamTupleType, PAM_PPM_ALPHA_TUPLETYPE))
+            retval =  RGB255Alpha;
+        else if(streq(pamTupleType, PAM_PGM_TUPLETYPE))
+            retval =  GRAY255;
+        else if(streq(pamTupleType, PAM_PGM_ALPHA_TUPLETYPE))
+            retval =  GRAY255Alpha;
+        else
+            pm_error("Don't know how to convert tuple type '%s'.",
+                     pamTupleType);
+    } else {
+        if (streq(pamTupleType, PAM_PPM_TUPLETYPE))
+            retval =  RGB;
+        else if(streq(pamTupleType, PAM_PPM_ALPHA_TUPLETYPE))
+            retval =  RGBAlpha;
+        else if(streq(pamTupleType, PAM_PGM_TUPLETYPE))
+            retval =  GRAY;
+        else if(streq(pamTupleType, PAM_PGM_ALPHA_TUPLETYPE))
+            retval =  GRAYAlpha;
+        else
+            pm_error("Don't know how to convert tuple type '%s'.",
+                     pamTupleType);
+    }
 
-static void
-trqrGrayAlpha(const tuplen *  const tuplerown,
-              unsigned int    const cols,
-              unsigned char * const qoiRow) {
+    return retval;
+}
 
-    size_t qoiRowCursor;
-    unsigned int col;
 
-    for (col = 0, qoiRowCursor = 0; col < cols; ++col) {
-        unsigned char const qoiSample = tuplerown[col][0] * QOI_MAXVAL;
 
-        qoiRow[qoiRowCursor++] = qoiSample;
-        qoiRow[qoiRowCursor++] = qoiSample;
-        qoiRow[qoiRowCursor++] = qoiSample;
-        qoiRow[qoiRowCursor++] =
-            tuplerown[col][PAM_GRAY_TRN_PLANE] * QOI_MAXVAL;
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wreturn-type"
+
+static unsigned int
+channelCtFmTupleType(enum Tupletype const tupleType) {
+
+    switch (tupleType) {
+        case RGB:
+          return 3;
+        case RGB255:
+          return 3;
+        case RGBAlpha:
+          return 4;
+        case RGB255Alpha:
+          return 4;
+        case BW:
+        case GRAY:
+          return 3;
+        case GRAY255:
+          return 3;
+        case BWAlpha:
+        case GRAYAlpha:
+          return 4;
+        case GRAY255Alpha:
+          return 4;
     }
 }
+#pragma GCC diagnostic pop
+
+
+
+static qoi_Rgba
+pxFmTuple(tuple          const tuple0,
+          const sample * const sampleMap,
+          enum Tupletype const tupleType) {
+/*----------------------------------------------------------------------------
+   Convert PAM tuple to qoi rgba pixel struct
+-----------------------------------------------------------------------------*/
+    qoi_Rgba px;
+
+    switch (tupleType) {
+    case RGB:
+        px.rgba.r = sampleMap[tuple0[PAM_RED_PLANE]];
+        px.rgba.g = sampleMap[tuple0[PAM_GRN_PLANE]];
+        px.rgba.b = sampleMap[tuple0[PAM_BLU_PLANE]];
+        px.rgba.a = 255;
+        break;
+    case RGB255:
+        px.rgba.r = tuple0[PAM_RED_PLANE];
+        px.rgba.g = tuple0[PAM_GRN_PLANE];
+        px.rgba.b = tuple0[PAM_BLU_PLANE];
+        px.rgba.a = 255;
+        break;
+    case RGBAlpha:
+        px.rgba.r = sampleMap[tuple0[PAM_RED_PLANE]];
+        px.rgba.g = sampleMap[tuple0[PAM_GRN_PLANE]];
+        px.rgba.b = sampleMap[tuple0[PAM_BLU_PLANE]];
+        px.rgba.a = sampleMap[tuple0[PAM_TRN_PLANE]];
+        break;
+    case RGB255Alpha:
+        px.rgba.r = tuple0[PAM_RED_PLANE];
+        px.rgba.g = tuple0[PAM_GRN_PLANE];
+        px.rgba.b = tuple0[PAM_BLU_PLANE];
+        px.rgba.a = tuple0[PAM_TRN_PLANE];
+        break;
+    case BW:
+    case GRAY : {
+        unsigned char const qoiSample = sampleMap[tuple0[0]];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = 255;
+    } break;
+    case GRAY255: {
+        unsigned char const qoiSample = tuple0[0];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = 255;
+    } break;
+    case BWAlpha:
+    case GRAYAlpha: {
+        unsigned char const qoiSample = sampleMap[tuple0[0]];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = sampleMap[tuple0[PAM_GRAY_TRN_PLANE]];
+    } break;
+    case GRAY255Alpha: {
+        unsigned char const qoiSample = tuple0[0];
+        px.rgba.r = qoiSample;
+        px.rgba.g = qoiSample;
+        px.rgba.b = qoiSample;
+        px.rgba.a = tuple0[PAM_GRAY_TRN_PLANE];
+    } break;
+    }
 
+    return px;
+}
 
 
-static Trqr *
-trqrForTupleType(const char * const tupleType) {
 
-    if (streq(tupleType, PAM_PPM_TUPLETYPE))
-        return &trqrRgb;
-    else if(streq(tupleType, PAM_PPM_ALPHA_TUPLETYPE))
-        return &trqrRgba;
-    else if(streq(tupleType, PAM_PBM_TUPLETYPE) ||
-            streq(tupleType, PAM_PGM_TUPLETYPE))
-        return &trqrGray;
-    else if(streq(tupleType, PAM_PBM_ALPHA_TUPLETYPE) ||
-            streq(tupleType, PAM_PGM_ALPHA_TUPLETYPE))
-        return &trqrGrayAlpha;
-    else {
-        pm_error("Don't know how to convert tuple type '%s'.", tupleType);
-        return NULL;  /* Suppress compiler warning */
+static void
+encodeNewPixel(qoi_Rgba const px,
+               qoi_Rgba const pxPrev) {
+
+    if (px.rgba.a == pxPrev.rgba.a) {
+        signed char const vr = px.rgba.r - pxPrev.rgba.r;
+        signed char const vg = px.rgba.g - pxPrev.rgba.g;
+        signed char const vb = px.rgba.b - pxPrev.rgba.b;
+
+        signed char const vgR = vr - vg;
+        signed char const vgB = vb - vg;
+
+        if (
+            vr > -3 && vr < 2 &&
+            vg > -3 && vg < 2 &&
+            vb > -3 && vb < 2
+            ) {
+            putchar(QOI_OP_DIFF |
+                    (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2));
+        } else if (
+            vgR >  -9 && vgR <  8 &&
+            vg  > -33 && vg  < 32 &&
+            vgB >  -9 && vgB <  8
+            ) {
+            putchar(QOI_OP_LUMA    | (vg   + 32));
+            putchar((vgR + 8) << 4 | (vgB +  8));
+        } else {
+            putchar(QOI_OP_RGB);
+            putchar(px.rgba.r);
+            putchar(px.rgba.g);
+            putchar(px.rgba.b);
+        }
+    } else {
+        putchar(QOI_OP_RGBA);
+        putchar(px.rgba.r);
+        putchar(px.rgba.g);
+        putchar(px.rgba.b);
+        putchar(px.rgba.a);
     }
 }
 
 
 
-int
-main(int argc, char **argv) {
+static void
+qoiEncode(FILE           * const ifP,
+          struct pam     * const inpamP) {
 
-    struct pam inpam;
-    Trqr * trqr;
-    qoi_Desc qoiDesc;
-    tuplen * tuplerown;
-    unsigned char * qoiRaster;
-    const unsigned char * qoiImage;
-    size_t qoiSz;
+    tuple * tuplerow;
     unsigned int row;
 
-    pm_proginit(&argc, (const char **)argv);
+    qoi_Rgba index[QOI_INDEX_SIZE];
+    qoi_Rgba pxPrev;
+    unsigned int run;
+    sample * sampleMap;
+    qoi_Desc qoiDesc;
 
-    pnm_readpaminit(stdin, &inpam, PAM_STRUCT_SIZE(tuple_type));
+    enum Tupletype const tupleType =
+      tupleTypeFmPam(inpamP->tuple_type, inpamP->maxval);
 
-    tuplerown = pnm_allocpamrown(&inpam);
+    if (inpamP->height > QOI_PIXELS_MAX / inpamP->width)
+        pm_error("Too many pixels for QOI: %u x %u (max is %u)",
+                 inpamP->height, inpamP->width, QOI_PIXELS_MAX);
 
     qoiDesc.colorspace = QOI_SRGB;
-    qoiDesc.width      = inpam.width;
-    qoiDesc.height     = inpam.height;
-    qoiDesc.channelCt  = inpam.depth <= 3 ? 3 : 4;
+    qoiDesc.width      = inpamP->width;
+    qoiDesc.height     = inpamP->height;
+    qoiDesc.channelCt  = channelCtFmTupleType(tupleType);
+
+    encodeQoiHeader(qoiDesc);
 
-    qoiRaster = malloc(qoiDesc.width * qoiDesc.height * qoiDesc.channelCt);
+    tuplerow = pnm_allocpamrow(inpamP);
 
-    if (!qoiRaster)
-        pm_error("Unable to get memory for QOI raster %u x %u x %u",
-                 qoiDesc.width, qoiDesc.height, qoiDesc.channelCt);
+    if (inpamP->maxval != 255)
+        createSampleMap(inpamP->maxval, &sampleMap);
 
-    trqr = trqrForTupleType(inpam.tuple_type);
+    qoi_clearQoiIndex(index);
+
+    pxPrev.rgba.r = 0;
+    pxPrev.rgba.g = 0;
+    pxPrev.rgba.b = 0;
+    pxPrev.rgba.a = 255;
 
     /* Read and convert rows. */
-    for (row = 0; row < inpam.height; ++row) {
-        pnm_readpamrown(&inpam, tuplerown);
-        trqr(tuplerown,
-             inpam.width,
-             &qoiRaster[row * inpam.width * qoiDesc.channelCt]);
+    for (row = 0, run = 0; row < inpamP->height; ++row) {
+        unsigned int col;
+
+        pnm_readpamrow(inpamP, tuplerow);
+
+        for (col = 0; col < inpamP->width; ++col) {
+            qoi_Rgba const px = pxFmTuple(tuplerow[col], sampleMap, tupleType);
+
+            if (px.v == pxPrev.v) {
+                ++run;
+                if (run == 62) {
+                    putchar(QOI_OP_RUN | (run - 1));
+                    run = 0;
+                }
+            } else {
+                unsigned int const indexPos = qoi_colorHash(px);
+
+                if (run > 0) {
+                    putchar(QOI_OP_RUN | (run - 1));
+                    run = 0;
+                }
+
+                if (index[indexPos].v == px.v) {
+                    putchar(QOI_OP_INDEX | indexPos);
+
+                } else {
+                    index[indexPos] = px;
+                    encodeNewPixel(px, pxPrev);
+                }
+            }
+            pxPrev = px;
+        }
     }
-    qoi_encode(qoiRaster, &qoiDesc, &qoiImage, &qoiSz);
 
-    pm_writefile(stdout, qoiImage, qoiSz);
+    if (run > 0)
+        putchar(QOI_OP_RUN | (run - 1));
 
-    free((void*)qoiImage);
-    free(qoiRaster);
-    pnm_freepamrown(tuplerown);
+    fwrite(qoi_padding, sizeof(qoi_padding), 1, stdout);
 
-    return 0;
+    if (inpamP->maxval != 255)
+        free(sampleMap);
+    pnm_freepamrow(tuplerow);
 }
 
 
 
+int
+main(int argc, const char **argv) {
+
+    struct CmdlineInfo cmdline;
+    struct pam inpam;
+    FILE * ifP;
+
+    pm_proginit(&argc, argv);
+
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
+
+    pnm_readpaminit(ifP, &inpam, PAM_STRUCT_SIZE(tuple_type));
+
+    qoiEncode(ifP, &inpam);
+
+    return 0;
+}
+
+
diff --git a/converter/other/qoi.c b/converter/other/qoi.c
deleted file mode 100644
index 4fbd7570..00000000
--- a/converter/other/qoi.c
+++ /dev/null
@@ -1,365 +0,0 @@
-/*
-
-QOI - The "Quite OK Image" format for fast, lossless image compression
-
-Dominic Szablewski - https://phoboslab.org
-
-
-LICENSE: The MIT License(MIT)
-
-Copyright(c) 2021 Dominic Szablewski
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of
-this software and associated documentation files(the "Software"), to deal in
-the Software without restriction, including without limitation the rights to
-use, copy, modify, merge, publish, distribute, sublicense, and / or sell copies
-of the Software, and to permit persons to whom the Software is furnished to do
-so, subject to the following conditions :
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
-
-*/
-
-
-#include <assert.h>
-#include <stdlib.h>
-#include <string.h>
-
-#include "pm.h"
-#include "mallocvar.h"
-#include "qoi.h"
-
-#define ZEROARRAY(a) memset((a),0,sizeof(a))
-
-#define QOI_OP_INDEX  0x00 /* 00xxxxxx */
-#define QOI_OP_DIFF   0x40 /* 01xxxxxx */
-#define QOI_OP_LUMA   0x80 /* 10xxxxxx */
-#define QOI_OP_RUN    0xc0 /* 11xxxxxx */
-#define QOI_OP_RGB    0xfe /* 11111110 */
-#define QOI_OP_RGBA   0xff /* 11111111 */
-
-#define QOI_MASK_2    0xc0 /* 11000000 */
-
-#define QOI_COLOR_HASH(C) (C.rgba.r*3 + C.rgba.g*5 + C.rgba.b*7 + C.rgba.a*11)
-#define QOI_MAGIC \
-    (((unsigned int)'q') << 24 | ((unsigned int)'o') << 16 | \
-     ((unsigned int)'i') <<  8 | ((unsigned int)'f'))
-#define QOI_HEADER_SIZE 14
-
-/* 2GB is the max file size that this implementation can safely handle. We
-   guard against anything larger than that, assuming the worst case with 5
-   bytes per pixel, rounded down to a nice clean value. 400 million pixels
-   ought to be enough for anybody.
- */
-#define QOI_PIXELS_MAX ((unsigned int)400000000)
-
-typedef union {
-    struct { unsigned char r, g, b, a; } rgba;
-    unsigned int v;
-} Rgba;
-
-static unsigned char const padding[8] = {0,0,0,0,0,0,0,1};
-
-static void
-write32(unsigned char * const bytes,
-        size_t *        const cursorP,
-        unsigned int    const v) {
-
-    bytes[(*cursorP)++] = (0xff000000 & v) >> 24;
-    bytes[(*cursorP)++] = (0x00ff0000 & v) >> 16;
-    bytes[(*cursorP)++] = (0x0000ff00 & v) >> 8;
-    bytes[(*cursorP)++] = (0x000000ff & v);
-}
-
-
-
-static unsigned int
-read32(const unsigned char * const bytes,
-       size_t *              const cursorP) {
-
-    unsigned int a = bytes[(*cursorP)++];
-    unsigned int b = bytes[(*cursorP)++];
-    unsigned int c = bytes[(*cursorP)++];
-    unsigned int d = bytes[(*cursorP)++];
-
-    return a << 24 | b << 16 | c << 8 | d;
-}
-
-
-
-static void
-encodeQoiHeader(unsigned char * const bytes,
-                qoi_Desc        const qoiDesc,
-                size_t *        const cursorP) {
-
-    write32(bytes, cursorP, QOI_MAGIC);
-    write32(bytes, cursorP, qoiDesc.width);
-    write32(bytes, cursorP, qoiDesc.height);
-    bytes[(*cursorP)++] = qoiDesc.channelCt;
-    bytes[(*cursorP)++] = qoiDesc.colorspace;
-}
-
-
-
-static void
-encodeNewPixel(Rgba            const px,
-               Rgba            const pxPrev,
-               unsigned char * const bytes,
-               size_t *        const cursorP) {
-
-    if (px.rgba.a == pxPrev.rgba.a) {
-        signed char const vr = px.rgba.r - pxPrev.rgba.r;
-        signed char const vg = px.rgba.g - pxPrev.rgba.g;
-        signed char const vb = px.rgba.b - pxPrev.rgba.b;
-
-        signed char const vgR = vr - vg;
-        signed char const vgB = vb - vg;
-
-        if (
-            vr > -3 && vr < 2 &&
-            vg > -3 && vg < 2 &&
-            vb > -3 && vb < 2
-            ) {
-            bytes[(*cursorP)++] = QOI_OP_DIFF |
-                (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2);
-        } else if (
-            vgR >  -9 && vgR <  8 &&
-            vg  > -33 && vg  < 32 &&
-            vgB >  -9 && vgB <  8
-            ) {
-            bytes[(*cursorP)++] = QOI_OP_LUMA     | (vg   + 32);
-            bytes[(*cursorP)++] = (vgR + 8) << 4 | (vgB +  8);
-        } else {
-            bytes[(*cursorP)++] = QOI_OP_RGB;
-            bytes[(*cursorP)++] = px.rgba.r;
-            bytes[(*cursorP)++] = px.rgba.g;
-            bytes[(*cursorP)++] = px.rgba.b;
-        }
-    } else {
-        bytes[(*cursorP)++] = QOI_OP_RGBA;
-        bytes[(*cursorP)++] = px.rgba.r;
-        bytes[(*cursorP)++] = px.rgba.g;
-        bytes[(*cursorP)++] = px.rgba.b;
-        bytes[(*cursorP)++] = px.rgba.a;
-    }
-}
-
-
-
-void
-qoi_encode(const unsigned char *  const pixels,
-           const qoi_Desc *       const descP,
-           const unsigned char ** const qoiImageP,
-           size_t *               const outLenP) {
-
-    size_t cursor;
-    unsigned int i, maxSize, run;
-    unsigned int pxLen, pxEnd, pxPos;
-    unsigned char * bytes;
-    Rgba index[64];
-    Rgba px, pxPrev;
-
-    assert(pixels);
-    assert(descP);
-    assert(outLenP);
-    assert(descP->width > 0);
-    assert(descP->height > 0);
-    assert(descP->channelCt >= 3 && descP->channelCt <= 4);
-    assert(descP->colorspace == QOI_SRGB || descP->colorspace == QOI_LINEAR);
-
-    if (descP->height >= QOI_PIXELS_MAX / descP->width)
-        pm_error("Too many pixles for OQI: %u x %u (max is %u",
-                 descP->height, descP->width, QOI_PIXELS_MAX);
-
-    maxSize =
-        descP->width * descP->height * (descP->channelCt + 1) +
-        QOI_HEADER_SIZE + sizeof(padding);
-
-    MALLOCARRAY(bytes, maxSize);
-    if (!bytes)
-        pm_error("Cannot allocate %u bytes", maxSize);
-
-    cursor = 0;
-
-    encodeQoiHeader(bytes, *descP, &cursor);
-
-    ZEROARRAY(index);
-
-    run = 0;
-    pxPrev.rgba.r = 0;
-    pxPrev.rgba.g = 0;
-    pxPrev.rgba.b = 0;
-    pxPrev.rgba.a = 255;
-    px = pxPrev;
-
-    pxLen = descP->width * descP->height * descP->channelCt;
-    pxEnd = pxLen - descP->channelCt;
-
-    for (pxPos = 0; pxPos < pxLen; pxPos += descP->channelCt) {
-        px.rgba.r = pixels[pxPos + 0];
-        px.rgba.g = pixels[pxPos + 1];
-        px.rgba.b = pixels[pxPos + 2];
-
-        if (descP->channelCt == 4) {
-            px.rgba.a = pixels[pxPos + 3];
-        }
-
-        if (px.v == pxPrev.v) {
-            ++run;
-            if (run == 62 || pxPos == pxEnd) {
-                bytes[cursor++] = QOI_OP_RUN | (run - 1);
-                run = 0;
-            }
-        } else {
-            unsigned int const indexPos = QOI_COLOR_HASH(px) % 64;
-
-            if (run > 0) {
-                bytes[cursor++] = QOI_OP_RUN | (run - 1);
-                run = 0;
-            }
-
-            if (index[indexPos].v == px.v) {
-                bytes[cursor++] = QOI_OP_INDEX | indexPos;
-            } else {
-                index[indexPos] = px;
-
-                encodeNewPixel(px, pxPrev, bytes, &cursor);
-            }
-        }
-        pxPrev = px;
-    }
-
-    for (i = 0; i < sizeof(padding); ++i)
-        bytes[cursor++] = padding[i];
-
-    *qoiImageP = bytes;
-    *outLenP   = cursor;
-}
-
-
-
-static void
-decodeQoiHeader(const unsigned char * const qoiImage,
-                qoi_Desc *            const qoiDescP,
-                size_t *              const cursorP) {
-
-    unsigned int headerMagic;
-
-    headerMagic          = read32(qoiImage, cursorP);
-    qoiDescP->width      = read32(qoiImage, cursorP);
-    qoiDescP->height     = read32(qoiImage, cursorP);
-    qoiDescP->channelCt  = qoiImage[(*cursorP)++];
-    qoiDescP->colorspace = qoiImage[(*cursorP)++];
-
-    if (qoiDescP->width == 0)
-        pm_error("Invalid QOI image: width is zero");
-    if (qoiDescP->height == 0)
-        pm_error("Invalid QOI image: height is zero");
-    if (qoiDescP->channelCt != 3 && qoiDescP->channelCt != 4)
-        pm_error("Invalid QOI image: channel count is %u.  "
-                 "Only 3 and 4 are valid", qoiDescP->channelCt);
-    if (qoiDescP->colorspace != QOI_SRGB && qoiDescP->colorspace != QOI_LINEAR)
-        pm_error("Invalid QOI image: colorspace code is %u.  "
-                 "Only %u (SRGB) and %u (LINEAR) are valid",
-                 qoiDescP->colorspace, QOI_SRGB, QOI_LINEAR);
-    if (headerMagic != QOI_MAGIC)
-        pm_error("Invalid QOI image: Where the magic number 0x%04x "
-                 "should be, there is 0x%04x",
-                 QOI_MAGIC, headerMagic);
-    if (qoiDescP->height >= QOI_PIXELS_MAX / qoiDescP->width)
-        pm_error ("Invalid QOI image: %u x %u is More than %u pixels",
-                  qoiDescP->width, qoiDescP->height, QOI_PIXELS_MAX);
-}
-
-
-
-void
-qoi_decode(const unsigned char *  const qoiImage,
-           size_t                 const size,
-           qoi_Desc *             const qoiDescP,
-           const unsigned char ** const qoiRasterP) {
-
-    unsigned int const chunksLen = size - sizeof(padding);
-
-    unsigned char * pixels;
-    Rgba index[64];
-    Rgba px;
-    unsigned int pxLen, pxPos;
-    size_t cursor;
-    unsigned int run;
-
-    assert(qoiImage);
-    assert(qoiDescP);
-    assert(size >= QOI_HEADER_SIZE + sizeof(padding));
-
-    cursor = 0;
-
-    decodeQoiHeader(qoiImage, qoiDescP, &cursor);
-
-    pxLen = qoiDescP->width * qoiDescP->height * qoiDescP->channelCt;
-    MALLOCARRAY(pixels, pxLen);
-    if (!pixels)
-        pm_error("Failed to allocate %u bytes for %u x %u x %u QOI raster",
-                 pxLen,
-                 qoiDescP->width, qoiDescP->height, qoiDescP->channelCt);
-
-    ZEROARRAY(index);
-    px.rgba.r = 0;
-    px.rgba.g = 0;
-    px.rgba.b = 0;
-    px.rgba.a = 255;
-
-    for (pxPos = 0, run = 0; pxPos < pxLen; pxPos += qoiDescP->channelCt) {
-        if (run > 0) {
-            --run;
-        } else if (cursor < chunksLen) {
-            unsigned char const b1 = qoiImage[cursor++];
-
-            if (b1 == QOI_OP_RGB) {
-                px.rgba.r = qoiImage[cursor++];
-                px.rgba.g = qoiImage[cursor++];
-                px.rgba.b = qoiImage[cursor++];
-            } else if (b1 == QOI_OP_RGBA) {
-                px.rgba.r = qoiImage[cursor++];
-                px.rgba.g = qoiImage[cursor++];
-                px.rgba.b = qoiImage[cursor++];
-                px.rgba.a = qoiImage[cursor++];
-            } else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) {
-                px = index[b1];
-            } else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) {
-                px.rgba.r += ((b1 >> 4) & 0x03) - 2;
-                px.rgba.g += ((b1 >> 2) & 0x03) - 2;
-                px.rgba.b += ( b1       & 0x03) - 2;
-            } else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) {
-                unsigned char const b2 = qoiImage[cursor++];
-                unsigned char const vg = (b1 & 0x3f) - 32;
-                px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f);
-                px.rgba.g += vg;
-                px.rgba.b += vg - 8 +  (b2       & 0x0f);
-            } else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) {
-                run = (b1 & 0x3f);
-            }
-
-            index[QOI_COLOR_HASH(px) % 64] = px;
-        }
-
-        pixels[pxPos + 0] = px.rgba.r;
-        pixels[pxPos + 1] = px.rgba.g;
-        pixels[pxPos + 2] = px.rgba.b;
-
-        if (qoiDescP->channelCt == 4)
-            pixels[pxPos + 3] = px.rgba.a;
-    }
-
-    *qoiRasterP = pixels;
-}
-
-
-
diff --git a/converter/other/qoi.h b/converter/other/qoi.h
index 2886a777..52ee95e2 100644
--- a/converter/other/qoi.h
+++ b/converter/other/qoi.h
@@ -27,254 +27,75 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 SOFTWARE.
 
+*/
 
--- About
-
-QOI encodes and decodes images in a lossless format. Compared to stb_image and
-stb_image_write QOI offers 20x-50x faster encoding, 3x-4x faster decoding and
-20% better compression.
-
-
--- Synopsis
-
-// Encode and store an RGBA buffer to the file system. The qoi_desc describes
-// the input pixel data.
-qoi_write("image_new.qoi", rgba_pixels, &(qoi_desc){
-    .width = 1920,
-    .height = 1080,
-    .channels = 4,
-    .colorspace = QOI_SRGB
-});
-
-// Load and decode a QOI image from the file system into a 32bbp RGBA buffer.
-// The qoi_desc struct will be filled with the width, height, number of channels
-// and colorspace read from the file header.
-qoi_desc desc;
-void *rgba_pixels = qoi_read("image.qoi", &desc, 4);
-
-
-
--- Documentation
-
-This library provides the following functions;
-- qoi_read    -- read and decode a QOI file
-- qoi_decode  -- decode the raw bytes of a QOI image from memory
-- qoi_write   -- encode and write a QOI file
-- qoi_encode  -- encode an rgba buffer into a QOI image in memory
-
-See the function declaration below for the signature and more information.
-
-This library uses malloc() and free(). To supply your own malloc implementation
-you can define QOI_MALLOC and QOI_FREE before including this library.
-
-This library uses memset() to zero-initialize the index. To supply your own
-implementation you can define QOI_ZEROARR before including this library.
-
-
--- Data Format
-
-A QOI file has a 14 byte header, followed by any number of data "chunks" and an
-8-byte end marker.
-
-struct qoi_header_t {
-    char     magic[4];   // magic bytes "qoif"
-    uint32_t width;      // image width in pixels (BE)
-    uint32_t height;     // image height in pixels (BE)
-    uint8_t  channels;   // 3 = RGB, 4 = RGBA
-    uint8_t  colorspace; // 0 = sRGB with linear alpha, 1 = all channels linear
-};
-
-Images are encoded row by row, left to right, top to bottom. The decoder and
-encoder start with {r: 0, g: 0, b: 0, a: 255} as the previous pixel value. An
-image is complete when all pixels specified by width * height have been covered.
-
-Pixels are encoded as
- - a run of the previous pixel
- - an index into an array of previously seen pixels
- - a difference to the previous pixel value in r,g,b
- - full r,g,b or r,g,b,a values
-
-The color channels are assumed to not be premultiplied with the alpha channel
-("un-premultiplied alpha").
-
-A running array[64] (zero-initialized) of previously seen pixel values is
-maintained by the encoder and decoder. Each pixel that is seen by the encoder
-and decoder is put into this array at the position formed by a hash function of
-the color value. In the encoder, if the pixel value at the index matches the
-current pixel, this index position is written to the stream as QOI_OP_INDEX.
-The hash function for the index is:
-
-    index_position = (r * 3 + g * 5 + b * 7 + a * 11) % 64
-
-Each chunk starts with a 2- or 8-bit tag, followed by a number of data bits. The
-bit length of chunks is divisible by 8 - i.e. all chunks are byte aligned. All
-values encoded in these data bits have the most significant bit on the left.
-
-The 8-bit tags have precedence over the 2-bit tags. A decoder must check for the
-presence of an 8-bit tag first.
-
-The byte stream's end is marked with 7 0x00 bytes followed a single 0x01 byte.
-
-
-The possible chunks are:
-
-
-.- QOI_OP_INDEX ----------.
-|         Byte[0]         |
-|  7  6  5  4  3  2  1  0 |
-|-------+-----------------|
-|  0  0 |     index       |
-`-------------------------`
-2-bit tag b00
-6-bit index into the color index array: 0..63
-
-A valid encoder must not issue 2 or more consecutive QOI_OP_INDEX chunks to the
-same index. QOI_OP_RUN should be used instead.
 
+typedef enum {
+    QOI_SRGB = 0,
+    QOI_LINEAR = 1
+} qoi_Colorspace;
 
-.- QOI_OP_DIFF -----------.
-|         Byte[0]         |
-|  7  6  5  4  3  2  1  0 |
-|-------+-----+-----+-----|
-|  0  1 |  dr |  dg |  db |
-`-------------------------`
-2-bit tag b01
-2-bit   red channel difference from the previous pixel between -2..1
-2-bit green channel difference from the previous pixel between -2..1
-2-bit  blue channel difference from the previous pixel between -2..1
 
-The difference to the current channel values are using a wraparound operation,
-so "1 - 2" will result in 255, while "255 + 1" will result in 0.
+typedef struct {
+    unsigned int   width;
+    unsigned int   height;
+    unsigned int   channelCt;
+    qoi_Colorspace colorspace;
+} qoi_Desc;
 
-Values are stored as unsigned integers with a bias of 2. E.g. -2 is stored as
-0 (b00). 1 is stored as 3 (b11).
 
-The alpha value remains unchanged from the previous pixel.
 
+#define QOI_OP_INDEX  0x00 /* 00xxxxxx */
+#define QOI_OP_DIFF   0x40 /* 01xxxxxx */
+#define QOI_OP_LUMA   0x80 /* 10xxxxxx */
+#define QOI_OP_RUN    0xc0 /* 11xxxxxx */
+#define QOI_OP_RGB    0xfe /* 11111110 */
+#define QOI_OP_RGBA   0xff /* 11111111 */
 
-.- QOI_OP_LUMA -------------------------------------.
-|         Byte[0]         |         Byte[1]         |
-|  7  6  5  4  3  2  1  0 |  7  6  5  4  3  2  1  0 |
-|-------+-----------------+-------------+-----------|
-|  1  0 |  green diff     |   dr - dg   |  db - dg  |
-`---------------------------------------------------`
-2-bit tag b10
-6-bit green channel difference from the previous pixel -32..31
-4-bit   red channel difference minus green channel difference -8..7
-4-bit  blue channel difference minus green channel difference -8..7
+#define QOI_MASK_2    0xc0 /* 11000000 */
 
-The green channel is used to indicate the general direction of change and is
-encoded in 6 bits. The red and blue channels (dr and db) base their diffs off
-of the green channel difference and are encoded in 4 bits. I.e.:
-    dr_dg = (cur_px.r - prev_px.r) - (cur_px.g - prev_px.g)
-    db_dg = (cur_px.b - prev_px.b) - (cur_px.g - prev_px.g)
-
-The difference to the current channel values are using a wraparound operation,
-so "10 - 13" will result in 253, while "250 + 7" will result in 1.
-
-Values are stored as unsigned integers with a bias of 32 for the green channel
-and a bias of 8 for the red and blue channel.
-
-The alpha value remains unchanged from the previous pixel.
-
-
-.- QOI_OP_RUN ------------.
-|         Byte[0]         |
-|  7  6  5  4  3  2  1  0 |
-|-------+-----------------|
-|  1  1 |       run       |
-`-------------------------`
-2-bit tag b11
-6-bit run-length repeating the previous pixel: 1..62
-
-The run-length is stored with a bias of -1. Note that the run-lengths 63 and 64
-(b111110 and b111111) are illegal as they are occupied by the QOI_OP_RGB and
-QOI_OP_RGBA tags.
+#define QOI_HEADER_SIZE 14
 
+/* 2GB is the max file size that this implementation can safely handle. We
+   guard against anything larger than that, assuming the worst case with 5
+   bytes per pixel, rounded down to a nice clean value. 400 million pixels
+   ought to be enough for anybody.
+ */
+#define QOI_PIXELS_MAX 400000000
 
-.- QOI_OP_RGB ------------------------------------------.
-|         Byte[0]         | Byte[1] | Byte[2] | Byte[3] |
-|  7  6  5  4  3  2  1  0 | 7 .. 0  | 7 .. 0  | 7 .. 0  |
-|-------------------------+---------+---------+---------|
-|  1  1  1  1  1  1  1  0 |   red   |  green  |  blue   |
-`-------------------------------------------------------`
-8-bit tag b11111110
-8-bit   red channel value
-8-bit green channel value
-8-bit  blue channel value
+static unsigned int const qoi_pixels_max = (unsigned int) QOI_PIXELS_MAX;
 
-The alpha value remains unchanged from the previous pixel.
+#define QOI_MAXVAL 255
 
+#define QOI_INDEX_SIZE 64
 
-.- QOI_OP_RGBA ---------------------------------------------------.
-|         Byte[0]         | Byte[1] | Byte[2] | Byte[3] | Byte[4] |
-|  7  6  5  4  3  2  1  0 | 7 .. 0  | 7 .. 0  | 7 .. 0  | 7 .. 0  |
-|-------------------------+---------+---------+---------+---------|
-|  1  1  1  1  1  1  1  1 |   red   |  green  |  blue   |  alpha  |
-`-----------------------------------------------------------------`
-8-bit tag b11111111
-8-bit   red channel value
-8-bit green channel value
-8-bit  blue channel value
-8-bit alpha channel value
 
-*/
+typedef union {
+    struct { unsigned char r, g, b, a; } rgba;
+    unsigned int v;
+} qoi_Rgba;
 
+static __inline__ unsigned int
+qoi_colorHash(qoi_Rgba const x) {
 
-/* A pointer to a qoi_desc struct has to be supplied to all of qoi's
-   functions.  It describes either the input format (for qoi_write and
-   qoi_encode), or is filled with the description read from the file header
-   (for qoi_read and qoi_decode).
+    return
+        (x.rgba.r*3 + x.rgba.g*5 + x.rgba.b*7 + x.rgba.a*11) % QOI_INDEX_SIZE;
+}
 
-   The colorspace in this qoi_desc is an enum where
-      0 = sRGB, i.e. gamma scaled RGB channels and a linear alpha channel
-      1 = all channels are linear
+static __inline__ void
+qoi_clearQoiIndex(qoi_Rgba * index) {
 
-   You may use the constants QOI_SRGB or QOI_LINEAR. The colorspace is purely
-   informative. It will be saved to the file header, but does not affect how
-   chunks are en-/decoded.
-*/
+    memset(index, 0, QOI_INDEX_SIZE * sizeof(qoi_Rgba));
 
-typedef enum {
-    QOI_SRGB = 0,
-    QOI_LINEAR = 1
-} qoi_Colorspace;
-
-typedef struct {
-    unsigned int   width;
-    unsigned int   height;
-    unsigned int   channelCt;
-    qoi_Colorspace colorspace;
-} qoi_Desc;
+}
 
-/* Encode raw RGB or RGBA pixels into a QOI image in memory.
+#define QOI_MAGIC_SIZE 4
 
-   The function either returns NULL on failure (invalid parameters or malloc
-   failed) or a pointer to the encoded data on success. On success the out_len
-   is set to the size in bytes of the encoded data.
-
-   The returned qoi data should be free()d after use.
-*/
+static char const qoi_magic[QOI_MAGIC_SIZE + 1] = {'q','o','i','f','\0'};
 
-void
-qoi_encode(const unsigned char *  const data,
-           const qoi_Desc *       const descP,
-           const unsigned char ** const qoiImageP,
-           size_t *               const outLenP);
+#define QOI_PADDING_SIZE 8
 
-/* Decode a QOI image from memory.
-
-   The function either returns NULL on failure (invalid parameters or malloc
-   failed) or a pointer to the decoded pixels. On success, the qoi_Desc struct
-   is filled with the description from the file header.
-
-   The returned pixel data should be free()d after use.
-*/
+static unsigned char const qoi_padding[QOI_PADDING_SIZE] = {0,0,0,0,0,0,0,1};
 
-void
-qoi_decode(const unsigned char *  const qoiImage,
-           size_t                 const size,
-           qoi_Desc *             const descP,
-           const unsigned char ** const qoiRasterP);
 
 #endif
diff --git a/converter/other/qoitopam.c b/converter/other/qoitopam.c
index cb9fd925..af6817b7 100644
--- a/converter/other/qoitopam.c
+++ b/converter/other/qoitopam.c
@@ -1,101 +1,305 @@
 /*
- * This file is part of Netpbm (http://netpbm.sourceforge.org).
- * Copyright (c) 2022 cancername.
- *
- * 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, version 3.
- *
- * 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.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
-#include <stdbool.h>
+  qoitopam -  Converts from a QOI - The "Quite OK Image" format file to PAM
+
+  This program is part of Netpbm.
+
+  ---------------------------------------------------------------------
+
+
+  QOI - The "Quite OK Image" format for fast, lossless image compression
+
+  Decoder by Dominic Szablewski - https://phoboslab.org
+
+  -- LICENSE: The MIT License(MIT)
+
+  Copyright(c) 2021 Dominic Szablewski
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy
+  of this software and associated documentation files(the "Software"), to deal
+  in the Software without restriction, including without limitation the rights
+  to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
+  copies of the Software, and to permit persons to whom the Software is
+  furnished to do so, subject to the following conditions :
+
+  The above copyright notice and this permission notice shall be included in
+  all copies or substantial portions of the Software.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.IN NO EVENT SHALL THE
+  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+  SOFTWARE.
+
+  For more information on the format visit: https//qoiformat.org/ .
+
+  Modifications for Netpbm & PAM write routines by Akira F. Urushibata.
+*/
+
 #include <stdio.h>
-#include <stdlib.h>
-#include <malloc.h>
 #include <string.h>
+#include <assert.h>
 
 #include "pm.h"
 #include "pam.h"
 #include "mallocvar.h"
+#include "nstring.h"
+#include "shhopt.h"
+
 #include "qoi.h"
 
-#define QOI_MAXVAL 0xFF
+
+
+struct CmdlineInfo {
+    /* All the information the user supplied in the command line,
+       in a form easy for the program to use.
+    */
+    const char * inputFileName;  /* '-' if stdin */
+};
 
 
 
 static void
-readQoi(FILE *                 const ifP,
-        qoi_Desc *             const qoiDescP,
-        const unsigned char ** const qoiRasterP) {
+parseCommandLine(int                  argc,
+                 const char **        argv,
+                 struct CmdlineInfo * cmdlineP ) {
+/*----------------------------------------------------------------------------
+   Parse program command line described in Unix standard form by argc
+   and argv.  Return the information in the options as *cmdlineP.
 
-    size_t qoiSz;
-    const unsigned char * qoiImg;
+   If command line is internally inconsistent (invalid options, etc.),
+   issue error message to stderr and abort program.
 
-    /* Unfortunately, qoi.h does not implement a streaming decoder,
-       we need to read the whole stream into memory -- expensive.
-       We might be able to cheat here with mmap sometimes,
-       but it's not worth the effort.
-    */
-    pm_readfile(stdin, &qoiImg, &qoiSz);
+   Note that the strings we return are stored in the storage that
+   was passed to us as the argv array.  We also trash *argv.
+-----------------------------------------------------------------------------*/
+    optEntry * option_def;
+        /* Instructions to pm_optParseOptions3 on how to parse our options.
+         */
+    optStruct3 opt;
+
+    unsigned int option_def_index;
+
+    MALLOCARRAY(option_def, 100);
+
+    OPTENTINIT;
+
+    opt.opt_table = option_def;
+    opt.short_allowed = FALSE;  /* We have no short (old-fashioned) options */
+    opt.allowNegNum = FALSE;  /* We have no parms that are negative numbers */
+
+    pm_optParseOptions3(&argc, (char **)argv, opt, sizeof(opt), 0);
+        /* Uses and sets argc, argv, and some of *cmdlineP and others. */
+
+    if (argc-1 < 1)
+        cmdlineP->inputFileName = "-";
+    else if (argc-1 == 1)
+        cmdlineP->inputFileName = argv[1];
+    else
+        pm_error("Program takes at most one argument: input file name.  "
+            "you specified %d", argc-1);
+}
+
+
+
+static void
+readAndValidateMagic(FILE * const ifP){
+
+    char magicBuff[QOI_MAGIC_SIZE];
+    size_t charsReadCt;
+
+    charsReadCt = fread(magicBuff, 1, QOI_MAGIC_SIZE, ifP);
+
+    if (charsReadCt == 0)
+        pm_error("Input file is empty.");
+    else if (charsReadCt < QOI_MAGIC_SIZE || !MEMSEQ(&magicBuff, &qoi_magic)) {
+        assert(QOI_MAGIC_SIZE == 4);
+        pm_error("Invalid QOI image: does not start with magic number "
+                 "'%c%c%c%c'",
+                 qoi_magic[0], qoi_magic[1], qoi_magic[2], qoi_magic[3]);
+    }
+}
+
+
+/* The following two functions are from lib/pmfileio.c */
 
-    qoi_decode(qoiImg, qoiSz, qoiDescP, qoiRasterP);
+static void
+abortWithReadError(FILE * const ifP) {
+
+    if (feof(ifP))
+        pm_error("Unexpected end of input file");
+    else
+        pm_error("Error (not EOF) reading file.");
+}
+
+
+
+static unsigned char
+getcNofail(FILE * const ifP) {
+
+    int c;
+
+    c = getc(ifP);
+
+    if (c == EOF)
+        abortWithReadError(ifP);
 
-    free((void*)qoiImg);
+    return (unsigned char) c;
 }
 
 
 
 static void
-writeRaster(struct pam            const outpam,
-            const unsigned char * const qoiRaster) {
+decodeQoiHeader(FILE *     const ifP,
+                qoi_Desc * const qoiDescP) {
+
+    unsigned long int width, height;
+
+    readAndValidateMagic(ifP);
+
+    pm_readbiglongu(ifP, &width);
+    if (width == 0)
+        pm_error("Invalid QOI image: width is zero");
+    else
+        qoiDescP->width = width;
+
+    pm_readbiglongu(ifP, &height);
+    if (height == 0)
+        pm_error("Invalid QOI image: height is zero");
+    else if (height > QOI_PIXELS_MAX / width)
+        pm_error ("Invalid QOI image: %u x %u is more than %u pixels",
+                  (unsigned int) width, (unsigned int) height, QOI_PIXELS_MAX);
+    else
+        qoiDescP->height = height;
+
+    qoiDescP->channelCt = getcNofail(ifP);
+    if (qoiDescP->channelCt != 3 && qoiDescP->channelCt != 4)
+        pm_error("Invalid QOI image: channel count is %u.  "
+                 "Only 3 and 4 are valid", qoiDescP->channelCt);
+
+    qoiDescP->colorspace = getcNofail(ifP);
+    if (qoiDescP->colorspace != QOI_SRGB && qoiDescP->colorspace != QOI_LINEAR)
+        pm_error("Invalid QOI image: colorspace code is %u.  "
+                 "Only %u (SRGB) and %u (LINEAR) are valid",
+                 qoiDescP->colorspace, QOI_SRGB, QOI_LINEAR);
+}
 
-    unsigned int qoiRasterCursor;
+
+
+static void
+qoiDecode(FILE *       const ifP,
+          qoi_Desc *   const qoiDescP,
+          struct pam * const outpamP) {
+
+    qoi_Rgba index[QOI_INDEX_SIZE];
     unsigned int row;
+    qoi_Rgba px;
+    unsigned int run;
     tuple * tuplerow;
 
-    tuplerow = pnm_allocpamrow(&outpam);
+    assert(qoiDescP);
+    tuplerow = pnm_allocpamrow(outpamP);
 
-    qoiRasterCursor = 0;  /* initial value */
+    qoi_clearQoiIndex(index);
+    px.rgba.r = px.rgba.g = px.rgba.b = 0;
+    px.rgba.a = 255;
 
-    for (row = 0; row < outpam.height; ++row) {
+    for (row = 0, run = 0; row < outpamP->height; ++row) {
         unsigned int col;
 
-        for (col = 0; col < outpam.width; ++col) {
-            tuplerow[col][PAM_RED_PLANE] = qoiRaster[qoiRasterCursor++];
-            tuplerow[col][PAM_GRN_PLANE] = qoiRaster[qoiRasterCursor++];
-            tuplerow[col][PAM_BLU_PLANE] = qoiRaster[qoiRasterCursor++];
-            if (outpam.depth > 3)
-                tuplerow[col][PAM_TRN_PLANE] = qoiRaster[qoiRasterCursor++];
+        for (col = 0; col < outpamP->width; ++col) {
+            if (run > 0) {
+                 --run;
+            } else {
+                unsigned char const b1 = getcNofail(ifP);
+
+                if (b1 == QOI_OP_RGB) {
+                    px.rgba.r = getcNofail(ifP);
+                    px.rgba.g = getcNofail(ifP);
+                    px.rgba.b = getcNofail(ifP);
+                } else if (b1 == QOI_OP_RGBA) {
+                    px.rgba.r = getcNofail(ifP);
+                    px.rgba.g = getcNofail(ifP);
+                    px.rgba.b = getcNofail(ifP);
+                    px.rgba.a = getcNofail(ifP);
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_INDEX) {
+                  /* Official spec says 2 or more consecutive instances of
+                     QOI_OP_INDEX are not allowed, but we don't check */
+                    px = index[b1];
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_DIFF) {
+                    px.rgba.r += ((b1 >> 4) & 0x03) - 2;
+                    px.rgba.g += ((b1 >> 2) & 0x03) - 2;
+                    px.rgba.b += ( b1       & 0x03) - 2;
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_LUMA) {
+                    unsigned char const b2 = getcNofail(ifP);
+                    unsigned char const vg = (b1 & 0x3f) - 32;
+                    px.rgba.r += vg - 8 + ((b2 >> 4) & 0x0f);
+                    px.rgba.g += vg;
+                    px.rgba.b += vg - 8 +  (b2       & 0x0f);
+                } else if ((b1 & QOI_MASK_2) == QOI_OP_RUN) {
+                    run = (b1 & 0x3f);
+                }
+                /* register pixel in hash lookup array */
+                index[qoi_colorHash(px)] = px;
             }
-        pnm_writepamrow(&outpam, tuplerow);
+            tuplerow[col][PAM_RED_PLANE] = px.rgba.r;
+            tuplerow[col][PAM_GRN_PLANE] = px.rgba.g;
+            tuplerow[col][PAM_BLU_PLANE] = px.rgba.b;
+            if (qoiDescP->channelCt == 4)
+                tuplerow[col][PAM_TRN_PLANE] = px.rgba.a;
+        }
+        pnm_writepamrow(outpamP, tuplerow);
     }
+    if (run > 0)
+        pm_error("Invalid QOI image: %u (or more) extra pixels "
+                 "beyond end of image.", run);
 
     pnm_freepamrow(tuplerow);
 }
 
 
 
+static void
+readAndValidatePadding(FILE * const ifP){
+
+    unsigned char padBuff[QOI_PADDING_SIZE];
+    size_t charsReadCt;
+
+    charsReadCt = fread(padBuff, 1, QOI_PADDING_SIZE, ifP);
+
+    if(charsReadCt < QOI_PADDING_SIZE) {
+        pm_error("Invalid QOI image.  Error reading final 8-byte padding.  "
+                 "Premature end of file.");
+    } else if (!MEMSEQ(&padBuff, &qoi_padding))
+        pm_error("Invalid QOI image.  Final 8-byte padding incorrect.");
+    else if (fgetc(ifP) != EOF)
+        pm_error("Invalid QOI image.  "
+                 "Extraneous bytes after final 8-byte padding.");
+}
+
+
+
 int
-main(int argc, char **argv) {
+main(int argc, const char **argv) {
 
-    const unsigned char * qoiRaster;
+    struct CmdlineInfo cmdline;
     qoi_Desc qoiDesc;
     struct pam outpam;
+    FILE * ifP;
+
+    pm_proginit(&argc, argv);
 
-    pm_proginit(&argc, (const char **)argv);
+    parseCommandLine(argc, argv, &cmdline);
+
+    ifP = pm_openr(cmdline.inputFileName);
 
     outpam.size        = sizeof(struct pam);
     outpam.len         = PAM_STRUCT_SIZE(tuple_type);
     outpam.maxval      = QOI_MAXVAL;
     outpam.plainformat = 0;
 
-    readQoi(stdin, &qoiDesc, &qoiRaster);
+    decodeQoiHeader(ifP, &qoiDesc);
 
     outpam.depth  = qoiDesc.channelCt == 3 ? 3 : 4;
     outpam.width  = qoiDesc.width;
@@ -109,10 +313,11 @@ main(int argc, char **argv) {
         strcpy(outpam.tuple_type, PAM_PPM_ALPHA_TUPLETYPE);
 
     pnm_writepaminit(&outpam);
+    qoiDecode(ifP, &qoiDesc, &outpam);
 
-    writeRaster(outpam, qoiRaster);
-
-    free((void*)qoiRaster);
+    readAndValidatePadding(ifP);
 
     return 0;
 }
+
+