/*
 * xtiff - view a TIFF file in an X window
 *
 * Dan Sears
 * Chris Sears
 *
 * Copyright 1991 by Digital Equipment Corporation, Maynard, Massachusetts.
 *
 *                      All Rights Reserved
 *
 * Permission to use, copy, modify, and distribute this software and its
 * documentation for any purpose and without fee is hereby granted,
 * provided that the above copyright notice appear in all copies and that
 * both that copyright notice and this permission notice appear in
 * supporting documentation, and that the name of Digital not be
 * used in advertising or publicity pertaining to distribution of the
 * software without specific, written prior permission.
 *
 * DIGITAL DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING
 * ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT SHALL
 * DIGITAL BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR
 * ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION,
 * ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS
 * SOFTWARE.
 *
 * Revision 1.0  90/05/07
 *      Initial release.
 * Revision 2.0  90/12/20
 *      Converted to use the Athena Widgets and the Xt Intrinsics.
 *
 * Notes:
 *
 * According to the TIFF 5.0 Specification, it is possible to have
 * both a TIFFTAG_COLORMAP and a TIFFTAG_COLORRESPONSECURVE.  This
 * doesn't make sense since a TIFFTAG_COLORMAP is 16 bits wide and
 * a TIFFTAG_COLORRESPONSECURVE is tfBitsPerSample bits wide for each
 * channel.  This is probably a bug in the specification.
 * In this case, TIFFTAG_COLORRESPONSECURVE is ignored.
 * This might make sense if TIFFTAG_COLORMAP was 8 bits wide.
 *
 * TIFFTAG_COLORMAP is often incorrectly written as ranging from
 * 0 to 255 rather than from 0 to 65535.  CheckAndCorrectColormap()
 * takes care of this.
 *
 * Only ORIENTATION_TOPLEFT is supported correctly.  This is the
 * default TIFF and X orientation.  Other orientations will be
 * displayed incorrectly.
 *
 * There is no support for or use of 3/3/2 DirectColor visuals.
 * TIFFTAG_MINSAMPLEVALUE and TIFFTAG_MAXSAMPLEVALUE are not supported.
 *
 * Only TIFFTAG_BITSPERSAMPLE values that are 1, 2, 4 or 8 are supported.
 */
#include <X11/Intrinsic.h>
#include <X11/Shell.h>
#include <X11/StringDefs.h>
#include <X11/Xatom.h>
#include <X11/Xaw/Form.h>
#include <X11/Xaw/Label.h>
#include <X11/Xaw/List.h>
#include <X11/Xproto.h>
#include <X11/cursorfont.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <tiffio.h>
#define XK_MISCELLANY
#include "xtifficon.h"
#include <X11/keysymdef.h>

#define TIFF_GAMMA "2.2" /* default gamma from the TIFF 5.0 spec */
#define ROUND(x) (uint16_t)((x) + 0.5)
#define SCALE(x, s) (((x) * 65535L) / (s))
#define MCHECK(m)                                                              \
    if (!m)                                                                    \
    {                                                                          \
        fprintf(stderr, "malloc failed\n");                                    \
        exit(0);                                                               \
    }
#define MIN(a, b) (((a) < (b)) ? (a) : (b))
#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define VIEWPORT_WIDTH 700
#define VIEWPORT_HEIGHT 500
#define KEY_TRANSLATE 20

#ifdef __STDC__
#define PP(args) args
#else
#define PP(args) ()
#endif

int main PP((int argc, char **argv));
void OpenTIFFFile PP((void));
void GetTIFFHeader PP((void));
void SetNameLabel PP((void));
void CheckAndCorrectColormap PP((void));
void SimpleGammaCorrection PP((void));
void GetVisual PP((void));
Boolean SearchVisualList PP((int image_depth, int visual_class,
                             Visual **visual));
void GetTIFFImage PP((void));
void CreateXImage PP((void));
XtCallbackProc SelectProc PP((Widget w, caddr_t unused_1, caddr_t unused_2));
void QuitProc PP((void));
void NextProc PP((void));
void PreviousProc PP((void));
void PageProc PP((int direction));
void EventProc PP((Widget widget, caddr_t unused, XEvent *event));
void ResizeProc PP((void));
int XTiffErrorHandler PP((Display * display, XErrorEvent *error_event));
void Usage PP((void));

int xtVersion = XtSpecificationRelease; /* xtiff depends on R4 or higher */

/*
 * Xt data structures
 */
Widget shellWidget, formWidget, listWidget, labelWidget, imageWidget;

enum
{
    ButtonQuit = 0,
    ButtonPreviousPage = 1,
    ButtonNextPage = 2
};

String buttonStrings[] = {"Quit", "Previous", "Next"};

static XrmOptionDescRec shellOptions[] = {
    {"-help", "*help", XrmoptionNoArg, (caddr_t) "True"},
    {"-gamma", "*gamma", XrmoptionSepArg, NULL},
    {"-usePixmap", "*usePixmap", XrmoptionSepArg, NULL},
    {"-viewportWidth", "*viewportWidth", XrmoptionSepArg, NULL},
    {"-viewportHeight", "*viewportHeight", XrmoptionSepArg, NULL},
    {"-translate", "*translate", XrmoptionSepArg, NULL},
    {"-verbose", "*verbose", XrmoptionSepArg, NULL}};

typedef struct
{
    Boolean help;
    float gamma;
    Boolean usePixmap;
    uint32_t viewportWidth;
    uint32_t viewportHeight;
    int translate;
    Boolean verbose;
} AppData, *AppDataPtr;

AppData appData;

XtResource clientResources[] = {
    {"help", XtCBoolean, XtRBoolean, sizeof(Boolean),
     XtOffset(AppDataPtr, help), XtRImmediate, (XtPointer)False},
    {"gamma", "Gamma", XtRFloat, sizeof(float), XtOffset(AppDataPtr, gamma),
     XtRString, (XtPointer)TIFF_GAMMA},
    {"usePixmap", "UsePixmap", XtRBoolean, sizeof(Boolean),
     XtOffset(AppDataPtr, usePixmap), XtRImmediate, (XtPointer)True},
    {"viewportWidth", "ViewportWidth", XtRInt, sizeof(int),
     XtOffset(AppDataPtr, viewportWidth), XtRImmediate,
     (XtPointer)VIEWPORT_WIDTH},
    {"viewportHeight", "ViewportHeight", XtRInt, sizeof(int),
     XtOffset(AppDataPtr, viewportHeight), XtRImmediate,
     (XtPointer)VIEWPORT_HEIGHT},
    {"translate", "Translate", XtRInt, sizeof(int),
     XtOffset(AppDataPtr, translate), XtRImmediate, (XtPointer)KEY_TRANSLATE},
    {"verbose", "Verbose", XtRBoolean, sizeof(Boolean),
     XtOffset(AppDataPtr, verbose), XtRImmediate, (XtPointer)True}};

Arg formArgs[] = {{XtNresizable, True}};

Arg listArgs[] = {
    {XtNresizable, False},         {XtNborderWidth, 0},
    {XtNdefaultColumns, 3},        {XtNforceColumns, True},
    {XtNlist, (int)buttonStrings}, {XtNnumberStrings, XtNumber(buttonStrings)},
    {XtNtop, XtChainTop},          {XtNleft, XtChainLeft},
    {XtNbottom, XtChainTop},       {XtNright, XtChainLeft}};

Arg labelArgs[] = {{XtNresizable, False},   {XtNwidth, 200},
                   {XtNborderWidth, 0},     {XtNjustify, XtJustifyLeft},
                   {XtNtop, XtChainTop},    {XtNleft, XtChainLeft},
                   {XtNbottom, XtChainTop}, {XtNright, XtChainLeft}};

Arg imageArgs[] = {{XtNresizable, True},    {XtNborderWidth, 0},
                   {XtNtop, XtChainTop},    {XtNleft, XtChainLeft},
                   {XtNbottom, XtChainTop}, {XtNright, XtChainLeft}};

XtActionsRec actionsTable[] = {{"quit", QuitProc},
                               {"next", NextProc},
                               {"previous", PreviousProc},
                               {"notifyresize", ResizeProc}};

char translationsTable[] = "<Key>q:      quit() \n \
                            <Key>Q:      quit() \n \
                            <Message>WM_PROTOCOLS: quit()\n \
                            <Key>p:      previous() \n \
                            <Key>P:      previous() \n \
                            <Key>n:      next() \n \
                            <Key>N:      next() \n \
                            <Configure>: notifyresize()";

/*
 * X data structures
 */
Colormap xColormap;
Display *xDisplay;
Pixmap xImagePixmap;
Visual *xVisual;
XImage *xImage;
GC xWinGc;
int xImageDepth, xScreen, xRedMask, xGreenMask, xBlueMask,
    xOffset = 0, yOffset = 0, grabX = -1, grabY = -1;
unsigned char basePixel = 0;

/*
 * TIFF data structures
 */
TIFF *tfFile = NULL;
uint32_t tfImageWidth, tfImageHeight;
uint16_t tfBitsPerSample, tfSamplesPerPixel, tfPlanarConfiguration,
    tfPhotometricInterpretation, tfGrayResponseUnit, tfImageDepth,
    tfBytesPerRow;
int tfDirectory = 0, tfMultiPage = False;
double tfUnitMap,
    tfGrayResponseUnitMap[] = {-1, -10, -100, -1000, -10000, -100000};

/*
 * display data structures
 */
double *dRed, *dGreen, *dBlue;

/*
 * shared data structures
 */
uint16_t *redMap = NULL, *greenMap = NULL, *blueMap = NULL, *grayMap = NULL,
         colormapSize;
char *imageMemory;
char *fileName;

int main(int argc, char **argv)
{
    XSetWindowAttributes window_attributes;
    Widget widget_list[3];
    Arg args[5];

    setbuf(stdout, NULL);
    setbuf(stderr, NULL);

    shellWidget = XtInitialize(argv[0], "XTiff", shellOptions,
                               XtNumber(shellOptions), &argc, argv);

    XSetErrorHandler(XTiffErrorHandler);

    XtGetApplicationResources(
        shellWidget, &appData, (XtResourceList)clientResources,
        (Cardinal)XtNumber(clientResources), (ArgList)NULL, (Cardinal)0);

    if ((argc <= 1) || (argc > 2) || appData.help)
        Usage();

    if (appData.verbose == False)
    {
        TIFFSetErrorHandler(0);
        TIFFSetWarningHandler(0);
    }

    fileName = argv[1];

    xDisplay = XtDisplay(shellWidget);
    xScreen = DefaultScreen(xDisplay);

    OpenTIFFFile();
    GetTIFFHeader();
    SimpleGammaCorrection();
    GetVisual();
    GetTIFFImage();

    /*
     * Send visual, colormap, depth and iconPixmap to shellWidget.
     * Sending the visual to the shell is only possible with the advent of R4.
     */
    XtSetArg(args[0], XtNvisual, xVisual);
    XtSetArg(args[1], XtNcolormap, xColormap);
    XtSetArg(args[2], XtNdepth,
             xImageDepth == 1 ? DefaultDepth(xDisplay, xScreen) : xImageDepth);
    XtSetArg(args[3], XtNiconPixmap,
             XCreateBitmapFromData(xDisplay, RootWindow(xDisplay, xScreen),
                                   xtifficon_bits, xtifficon_width,
                                   xtifficon_height));
    XtSetArg(args[4], XtNallowShellResize, True);
    XtSetValues(shellWidget, args, 5);

    /*
     * widget instance hierarchy
     */
    formWidget = XtCreateManagedWidget("form", formWidgetClass, shellWidget,
                                       formArgs, XtNumber(formArgs));

    widget_list[0] = listWidget = XtCreateWidget(
        "list", listWidgetClass, formWidget, listArgs, XtNumber(listArgs));

    widget_list[1] = labelWidget = XtCreateWidget(
        "label", labelWidgetClass, formWidget, labelArgs, XtNumber(labelArgs));

    widget_list[2] = imageWidget = XtCreateWidget(
        "image", widgetClass, formWidget, imageArgs, XtNumber(imageArgs));

    XtManageChildren(widget_list, XtNumber(widget_list));

    /*
     * initial widget sizes - for small images let xtiff size itself
     */
    if (tfImageWidth >= appData.viewportWidth)
    {
        XtSetArg(args[0], XtNwidth, appData.viewportWidth);
        XtSetValues(shellWidget, args, 1);
    }
    if (tfImageHeight >= appData.viewportHeight)
    {
        XtSetArg(args[0], XtNheight, appData.viewportHeight);
        XtSetValues(shellWidget, args, 1);
    }

    XtSetArg(args[0], XtNwidth, tfImageWidth);
    XtSetArg(args[1], XtNheight, tfImageHeight);
    XtSetValues(imageWidget, args, 2);

    /*
     * formWidget uses these constraints but they are stored in the children.
     */
    XtSetArg(args[0], XtNfromVert, listWidget);
    XtSetValues(imageWidget, args, 1);
    XtSetArg(args[0], XtNfromHoriz, listWidget);
    XtSetValues(labelWidget, args, 1);

    SetNameLabel();

    XtAddCallback(listWidget, XtNcallback, (XtCallbackProc)SelectProc,
                  (XtPointer)NULL);

    XtAddActions(actionsTable, XtNumber(actionsTable));
    XtSetArg(args[0], XtNtranslations,
             XtParseTranslationTable(translationsTable));
    XtSetValues(formWidget, &args[0], 1);
    XtSetValues(imageWidget, &args[0], 1);

    /*
     * This is intended to be a little faster than going through
     * the translation manager.
     */
    XtAddEventHandler(imageWidget,
                      ExposureMask | ButtonPressMask | ButtonReleaseMask |
                          Button1MotionMask | KeyPressMask,
                      False, EventProc, NULL);

    XtRealizeWidget(shellWidget);

    window_attributes.cursor = XCreateFontCursor(xDisplay, XC_fleur);
    XChangeWindowAttributes(xDisplay, XtWindow(imageWidget), CWCursor,
                            &window_attributes);

    CreateXImage();

    XtMainLoop();

    return 0;
}

void OpenTIFFFile()
{
    if (tfFile != NULL)
        TIFFClose(tfFile);

    if ((tfFile = TIFFOpen(fileName, "r")) == NULL)
    {
        fprintf(appData.verbose ? stderr : stdout,
                "xtiff: can't open %s as a TIFF file\n", fileName);
        exit(0);
    }

    tfMultiPage = (TIFFLastDirectory(tfFile) ? False : True);
}

void GetTIFFHeader()
{
    register int i;

    if (!TIFFSetDirectory(tfFile, tfDirectory))
    {
        fprintf(stderr, "xtiff: can't seek to directory %d in %s\n",
                tfDirectory, fileName);
        exit(0);
    }

    TIFFGetField(tfFile, TIFFTAG_IMAGEWIDTH, &tfImageWidth);
    TIFFGetField(tfFile, TIFFTAG_IMAGELENGTH, &tfImageHeight);

    /*
     * If the following tags aren't present then use the TIFF defaults.
     */
    TIFFGetFieldDefaulted(tfFile, TIFFTAG_BITSPERSAMPLE, &tfBitsPerSample);
    TIFFGetFieldDefaulted(tfFile, TIFFTAG_SAMPLESPERPIXEL, &tfSamplesPerPixel);
    TIFFGetFieldDefaulted(tfFile, TIFFTAG_PLANARCONFIG, &tfPlanarConfiguration);
    TIFFGetFieldDefaulted(tfFile, TIFFTAG_GRAYRESPONSEUNIT,
                          &tfGrayResponseUnit);

    tfUnitMap = tfGrayResponseUnitMap[tfGrayResponseUnit];
    colormapSize = 1 << tfBitsPerSample;
    tfImageDepth = tfBitsPerSample * tfSamplesPerPixel;

    dRed = (double *)malloc(colormapSize * sizeof(double));
    dGreen = (double *)malloc(colormapSize * sizeof(double));
    dBlue = (double *)malloc(colormapSize * sizeof(double));
    MCHECK(dRed);
    MCHECK(dGreen);
    MCHECK(dBlue);

    /*
     * If TIFFTAG_PHOTOMETRIC is not present then assign a reasonable default.
     * The TIFF 5.0 specification doesn't give a default.
     */
    if (!TIFFGetField(tfFile, TIFFTAG_PHOTOMETRIC,
                      &tfPhotometricInterpretation))
    {
        if (tfSamplesPerPixel != 1)
            tfPhotometricInterpretation = PHOTOMETRIC_RGB;
        else if (tfBitsPerSample == 1)
            tfPhotometricInterpretation = PHOTOMETRIC_MINISBLACK;
        else if (TIFFGetField(tfFile, TIFFTAG_COLORMAP, &redMap, &greenMap,
                              &blueMap))
        {
            tfPhotometricInterpretation = PHOTOMETRIC_PALETTE;
            redMap = greenMap = blueMap = NULL;
        }
        else
            tfPhotometricInterpretation = PHOTOMETRIC_MINISBLACK;
    }

    /*
     * Given TIFFTAG_PHOTOMETRIC extract or create the response curves.
     */
    switch (tfPhotometricInterpretation)
    {
        case PHOTOMETRIC_RGB:
            redMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            greenMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            blueMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            MCHECK(redMap);
            MCHECK(greenMap);
            MCHECK(blueMap);
            for (i = 0; i < colormapSize; i++)
                dRed[i] = dGreen[i] = dBlue[i] =
                    (double)SCALE(i, colormapSize - 1);
            break;
        case PHOTOMETRIC_PALETTE:
            if (!TIFFGetField(tfFile, TIFFTAG_COLORMAP, &redMap, &greenMap,
                              &blueMap))
            {
                redMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
                greenMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
                blueMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
                MCHECK(redMap);
                MCHECK(greenMap);
                MCHECK(blueMap);
                for (i = 0; i < colormapSize; i++)
                    dRed[i] = dGreen[i] = dBlue[i] =
                        (double)SCALE(i, colormapSize - 1);
            }
            else
            {
                CheckAndCorrectColormap();
                for (i = 0; i < colormapSize; i++)
                {
                    dRed[i] = (double)redMap[i];
                    dGreen[i] = (double)greenMap[i];
                    dBlue[i] = (double)blueMap[i];
                }
            }
            break;
        case PHOTOMETRIC_MINISWHITE:
            redMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            greenMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            blueMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            MCHECK(redMap);
            MCHECK(greenMap);
            MCHECK(blueMap);
            for (i = 0; i < colormapSize; i++)
                dRed[i] = dGreen[i] = dBlue[i] =
                    (double)SCALE(colormapSize - 1 - i, colormapSize - 1);
            break;
        case PHOTOMETRIC_MINISBLACK:
            redMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            greenMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            blueMap = (uint16_t *)malloc(colormapSize * sizeof(uint16_t));
            MCHECK(redMap);
            MCHECK(greenMap);
            MCHECK(blueMap);
            for (i = 0; i < colormapSize; i++)
                dRed[i] = dGreen[i] = dBlue[i] =
                    (double)SCALE(i, colormapSize - 1);
            break;
        default:
            fprintf(stderr,
                    "xtiff: can't display photometric interpretation type %d\n",
                    tfPhotometricInterpretation);
            exit(0);
    }
}

void SetNameLabel()
{
    char buffer[BUFSIZ];
    Arg args[1];

    if (tfMultiPage)
        snprintf(buffer, sizeof(buffer), "%s - page %d", fileName, tfDirectory);
    else
        snprintf(buffer, sizeof(buffer), "%s", fileName);
    XtSetArg(args[0], XtNlabel, buffer);
    XtSetValues(labelWidget, args, 1);
}

/*
 * Many programs get TIFF colormaps wrong.  They use 8-bit colormaps instead of
 * 16-bit colormaps.  This function is a heuristic to detect and correct this.
 */
void CheckAndCorrectColormap()
{
    register int i;

    for (i = 0; i < colormapSize; i++)
        if ((redMap[i] > 255) || (greenMap[i] > 255) || (blueMap[i] > 255))
            return;

    for (i = 0; i < colormapSize; i++)
    {
        redMap[i] = SCALE(redMap[i], 255);
        greenMap[i] = SCALE(greenMap[i], 255);
        blueMap[i] = SCALE(blueMap[i], 255);
    }
    TIFFWarning(fileName, "Assuming 8-bit colormap");
}

void SimpleGammaCorrection()
{
    register int i;
    register double i_gamma = 1.0 / appData.gamma;

    for (i = 0; i < colormapSize; i++)
    {
        if (((tfPhotometricInterpretation == PHOTOMETRIC_MINISWHITE) &&
             (i == colormapSize - 1)) ||
            ((tfPhotometricInterpretation == PHOTOMETRIC_MINISBLACK) &&
             (i == 0)))
            redMap[i] = greenMap[i] = blueMap[i] = 0;
        else
        {
            redMap[i] = ROUND((pow(dRed[i] / 65535.0, i_gamma) * 65535.0));
            greenMap[i] = ROUND((pow(dGreen[i] / 65535.0, i_gamma) * 65535.0));
            blueMap[i] = ROUND((pow(dBlue[i] / 65535.0, i_gamma) * 65535.0));
        }
    }

    free(dRed);
    free(dGreen);
    free(dBlue);
}

static char *classNames[] = {"StaticGray",  "GrayScale", "StaticColor",
                             "PseudoColor", "TrueColor", "DirectColor"};

/*
 * Current limitation: the visual is set initially by the first file.
 * It cannot be changed.
 */
void GetVisual()
{
    XColor *colors = NULL;
    unsigned long *pixels = NULL;
    unsigned long i;

    switch (tfImageDepth)
    {
        /*
         * X really wants a 32-bit image with the fourth channel unused,
         * but the visual structure thinks it's 24-bit.  bitmap_unit is 32.
         */
        case 32:
        case 24:
            if (SearchVisualList(24, DirectColor, &xVisual) == False)
            {
                fprintf(stderr,
                        "xtiff: 24-bit DirectColor visual not available\n");
                exit(0);
            }

            colors = (XColor *)malloc(3 * colormapSize * sizeof(XColor));
            MCHECK(colors);

            for (i = 0; i < colormapSize; i++)
            {
                colors[i].pixel = (i << 16) + (i << 8) + i;
                colors[i].red = redMap[i];
                colors[i].green = greenMap[i];
                colors[i].blue = blueMap[i];
                colors[i].flags = DoRed | DoGreen | DoBlue;
            }

            xColormap = XCreateColormap(xDisplay, RootWindow(xDisplay, xScreen),
                                        xVisual, AllocAll);
            XStoreColors(xDisplay, xColormap, colors, colormapSize);
            break;
        case 8:
        case 4:
        case 2:
            /*
             * We assume that systems with 24-bit visuals also have 8-bit
             * visuals. We don't promote from 8-bit PseudoColor to 24/32 bit
             * DirectColor.
             */
            switch (tfPhotometricInterpretation)
            {
                case PHOTOMETRIC_MINISWHITE:
                case PHOTOMETRIC_MINISBLACK:
                    if (SearchVisualList((int)tfImageDepth, GrayScale,
                                         &xVisual) == True)
                        break;
                case PHOTOMETRIC_PALETTE:
                    if (SearchVisualList((int)tfImageDepth, PseudoColor,
                                         &xVisual) == True)
                        break;
                default:
                    fprintf(stderr,
                            "xtiff: Unsupported TIFF/X configuration\n");
                    exit(0);
            }

            colors = (XColor *)malloc(colormapSize * sizeof(XColor));
            MCHECK(colors);

            for (i = 0; i < colormapSize; i++)
            {
                colors[i].pixel = i;
                colors[i].red = redMap[i];
                colors[i].green = greenMap[i];
                colors[i].blue = blueMap[i];
                colors[i].flags = DoRed | DoGreen | DoBlue;
            }

            /*
             * xtiff's colormap allocation is private.  It does not attempt
             * to detect whether any existing colormap entries are suitable
             * for its use.  This will cause colormap flashing.  Furthermore,
             * background and foreground are taken from the environment.
             * For example, the foreground color may be red when the visual
             * is GrayScale.  If the colormap is completely populated,
             * Xt will not be able to allocate fg and bg.
             */
            if (tfImageDepth == 8)
                xColormap = XCreateColormap(
                    xDisplay, RootWindow(xDisplay, xScreen), xVisual, AllocAll);
            else
            {
                xColormap =
                    XCreateColormap(xDisplay, RootWindow(xDisplay, xScreen),
                                    xVisual, AllocNone);
                pixels = (unsigned long *)malloc(colormapSize *
                                                 sizeof(unsigned long));
                MCHECK(pixels);
                (void)XAllocColorCells(xDisplay, xColormap, True, NULL, 0,
                                       pixels, colormapSize);
                basePixel = (unsigned char)pixels[0];
                free(pixels);
            }
            XStoreColors(xDisplay, xColormap, colors, colormapSize);
            break;
        case 1:
            xImageDepth = 1;
            xVisual = DefaultVisual(xDisplay, xScreen);
            xColormap = DefaultColormap(xDisplay, xScreen);
            break;
        default:
            fprintf(stderr, "xtiff: unsupported image depth %d\n",
                    tfImageDepth);
            exit(0);
    }

    if (appData.verbose == True)
        fprintf(stderr, "%s: Using %d-bit %s visual.\n", fileName, xImageDepth,
                classNames[xVisual->class]);

    if (colors != NULL)
        free(colors);
    if (grayMap != NULL)
        free(grayMap);
    if (redMap != NULL)
        free(redMap);
    if (greenMap != NULL)
        free(greenMap);
    if (blueMap != NULL)
        free(blueMap);

    colors = NULL;
    grayMap = redMap = greenMap = blueMap = NULL;
}

/*
 * Search for an appropriate visual.  Promote where necessary.
 * Check to make sure that ENOUGH colormap entries are writeable.
 * basePixel was determined when XAllocColorCells() contiguously
 * allocated enough entries.  basePixel is used below in GetTIFFImage.
 */
Boolean SearchVisualList(image_depth, visual_class, visual)
int image_depth, visual_class;
Visual **visual;
{
    XVisualInfo template_visual, *visual_list, *vl;
    int i, n_visuals;

    template_visual.screen = xScreen;
    vl = visual_list = XGetVisualInfo(xDisplay, VisualScreenMask,
                                      &template_visual, &n_visuals);

    if (n_visuals == 0)
    {
        fprintf(stderr, "xtiff: visual list not available\n");
        exit(0);
    }

    for (i = 0; i < n_visuals; vl++, i++)
    {
        if ((vl->class == visual_class) && (vl->depth >= image_depth) &&
            (vl->visual->map_entries >= (1 << vl->depth)))
        {
            *visual = vl->visual;
            xImageDepth = vl->depth;
            xRedMask = vl->red_mask;
            xGreenMask = vl->green_mask;
            xBlueMask = vl->blue_mask;
            XFree((char *)visual_list);
            return True;
        }
    }

    XFree((char *)visual_list);
    return False;
}

void GetTIFFImage()
{
    int pixel_map[3], red_shift, green_shift, blue_shift;
    char *scan_line, *output_p, *input_p;
    uint32_t i, j;
    uint16_t s;

    scan_line = (char *)malloc(tfBytesPerRow = TIFFScanlineSize(tfFile));
    MCHECK(scan_line);

    if ((tfImageDepth == 32) || (tfImageDepth == 24))
    {
        output_p = imageMemory =
            (char *)malloc(tfImageWidth * tfImageHeight * 4);
        MCHECK(imageMemory);

        /*
         * Handle different color masks for different frame buffers.
         */
        if (ImageByteOrder(xDisplay) == LSBFirst)
        { /* DECstation 5000 */
            red_shift = pixel_map[0] =
                xRedMask == 0xFF000000
                    ? 3
                    : (xRedMask == 0xFF0000 ? 2 : (xRedMask == 0xFF00 ? 1 : 0));
            green_shift = pixel_map[1] =
                xGreenMask == 0xFF000000
                    ? 3
                    : (xGreenMask == 0xFF0000 ? 2
                                              : (xGreenMask == 0xFF00 ? 1 : 0));
            blue_shift = pixel_map[2] =
                xBlueMask == 0xFF000000
                    ? 3
                    : (xBlueMask == 0xFF0000 ? 2
                                             : (xBlueMask == 0xFF00 ? 1 : 0));
        }
        else
        { /* Ardent */
            red_shift = pixel_map[0] =
                xRedMask == 0xFF000000
                    ? 0
                    : (xRedMask == 0xFF0000 ? 1 : (xRedMask == 0xFF00 ? 2 : 3));
            green_shift = pixel_map[0] =
                xGreenMask == 0xFF000000
                    ? 0
                    : (xGreenMask == 0xFF0000 ? 1
                                              : (xGreenMask == 0xFF00 ? 2 : 3));
            blue_shift = pixel_map[0] =
                xBlueMask == 0xFF000000
                    ? 0
                    : (xBlueMask == 0xFF0000 ? 1
                                             : (xBlueMask == 0xFF00 ? 2 : 3));
        }

        if (tfPlanarConfiguration == PLANARCONFIG_CONTIG)
        {
            for (i = 0; i < tfImageHeight; i++)
            {
                if (TIFFReadScanline(tfFile, scan_line, i, 0) < 0)
                    break;
                for (input_p = scan_line, j = 0; j < tfImageWidth; j++)
                {
                    *(output_p + red_shift) = *input_p++;
                    *(output_p + green_shift) = *input_p++;
                    *(output_p + blue_shift) = *input_p++;
                    output_p += 4;
                    if (tfSamplesPerPixel == 4) /* skip the fourth channel */
                        input_p++;
                }
            }
        }
        else
        {
            for (s = 0; s < tfSamplesPerPixel; s++)
            {
                if (s == 3) /* skip the fourth channel */
                    continue;
                for (i = 0; i < tfImageHeight; i++)
                {
                    if (TIFFReadScanline(tfFile, scan_line, i, s) < 0)
                        break;
                    input_p = scan_line;
                    output_p =
                        imageMemory + (i * tfImageWidth * 4) + pixel_map[s];
                    for (j = 0; j < tfImageWidth; j++, output_p += 4)
                        *output_p = *input_p++;
                }
            }
        }
    }
    else
    {
        if (xImageDepth == tfImageDepth)
        {
            output_p = imageMemory =
                (char *)malloc(tfBytesPerRow * tfImageHeight);
            MCHECK(imageMemory);

            for (i = 0; i < tfImageHeight; i++, output_p += tfBytesPerRow)
                if (TIFFReadScanline(tfFile, output_p, i, 0) < 0)
                    break;
        }
        else if ((xImageDepth == 8) && (tfImageDepth == 4))
        {
            output_p = imageMemory =
                (char *)malloc(tfBytesPerRow * 2 * tfImageHeight + 2);
            MCHECK(imageMemory);

            /*
             * If a scanline is of odd size the inner loop below will overshoot.
             * This is handled very simply by recalculating the start point at
             * each scanline and padding imageMemory a little at the end.
             */
            for (i = 0; i < tfImageHeight; i++)
            {
                if (TIFFReadScanline(tfFile, scan_line, i, 0) < 0)
                    break;
                output_p = &imageMemory[i * tfImageWidth];
                input_p = scan_line;
                for (j = 0; j < tfImageWidth; j += 2, input_p++)
                {
                    *output_p++ = (*input_p >> 4) + basePixel;
                    *output_p++ = (*input_p & 0xf) + basePixel;
                }
            }
        }
        else if ((xImageDepth == 8) && (tfImageDepth == 2))
        {
            output_p = imageMemory =
                (char *)malloc(tfBytesPerRow * 4 * tfImageHeight + 4);
            MCHECK(imageMemory);

            for (i = 0; i < tfImageHeight; i++)
            {
                if (TIFFReadScanline(tfFile, scan_line, i, 0) < 0)
                    break;
                output_p = &imageMemory[i * tfImageWidth];
                input_p = scan_line;
                for (j = 0; j < tfImageWidth; j += 4, input_p++)
                {
                    *output_p++ = (*input_p >> 6) + basePixel;
                    *output_p++ = ((*input_p >> 4) & 3) + basePixel;
                    *output_p++ = ((*input_p >> 2) & 3) + basePixel;
                    *output_p++ = (*input_p & 3) + basePixel;
                }
            }
        }
        else if ((xImageDepth == 4) && (tfImageDepth == 2))
        {
            output_p = imageMemory =
                (char *)malloc(tfBytesPerRow * 2 * tfImageHeight + 2);
            MCHECK(imageMemory);

            for (i = 0; i < tfImageHeight; i++)
            {
                if (TIFFReadScanline(tfFile, scan_line, i, 0) < 0)
                    break;
                output_p = &imageMemory[i * tfBytesPerRow * 2];
                input_p = scan_line;
                for (j = 0; j < tfImageWidth; j += 4, input_p++)
                {
                    *output_p++ =
                        (((*input_p >> 6) << 4) | ((*input_p >> 4) & 3)) +
                        basePixel;
                    *output_p++ =
                        ((((*input_p >> 2) & 3) << 4) | (*input_p & 3)) +
                        basePixel;
                }
            }
        }
        else
        {
            fprintf(
                stderr,
                "xtiff: can't handle %d-bit TIFF file on an %d-bit display\n",
                tfImageDepth, xImageDepth);
            exit(0);
        }
    }

    free(scan_line);
}

void CreateXImage()
{
    XGCValues gc_values;
    GC bitmap_gc;

    xOffset = yOffset = 0;
    grabX = grabY = -1;

    xImage = XCreateImage(xDisplay, xVisual, xImageDepth,
                          xImageDepth == 1 ? XYBitmap : ZPixmap, /* offset */ 0,
                          (char *)imageMemory, tfImageWidth, tfImageHeight,
                          /* bitmap_pad */ 8, /* bytes_per_line */ 0);

    /*
     * libtiff converts LSB data into MSB but doesn't change the FillOrder tag.
     */
    if (xImageDepth == 1)
        xImage->bitmap_bit_order = MSBFirst;
    if (xImageDepth <= 8)
        xImage->byte_order = MSBFirst;

    /*
     * create an appropriate GC
     */
    gc_values.function = GXcopy;
    gc_values.plane_mask = AllPlanes;
    if (tfPhotometricInterpretation == PHOTOMETRIC_MINISBLACK)
    {
        gc_values.foreground = XWhitePixel(xDisplay, xScreen);
        gc_values.background = XBlackPixel(xDisplay, xScreen);
    }
    else
    {
        gc_values.foreground = XBlackPixel(xDisplay, xScreen);
        gc_values.background = XWhitePixel(xDisplay, xScreen);
    }
    xWinGc = XCreateGC(xDisplay, XtWindow(shellWidget),
                       GCFunction | GCPlaneMask | GCForeground | GCBackground,
                       &gc_values);

    /*
     * create the pixmap and load the image
     */
    if (appData.usePixmap == True)
    {
        xImagePixmap =
            XCreatePixmap(xDisplay, RootWindow(xDisplay, xScreen),
                          xImage->width, xImage->height, xImageDepth);

        /*
         * According to the O'Reilly X Protocol Reference Manual, page 53,
         * "A pixmap depth of one is always supported and listed, but windows
         * of depth one might not be supported."  Therefore we create a pixmap
         * of depth one and use XCopyPlane().  This is idiomatic.
         */
        if (xImageDepth == 1)
        {                             /* just pass the bits through */
            gc_values.foreground = 1; /* foreground describes set bits */
            gc_values.background = 0; /* background describes clear bits */
            bitmap_gc = XCreateGC(xDisplay, xImagePixmap,
                                  GCForeground | GCBackground, &gc_values);
            XPutImage(xDisplay, xImagePixmap, bitmap_gc, xImage, 0, 0, 0, 0,
                      xImage->width, xImage->height);
        }
        else
            XPutImage(xDisplay, xImagePixmap, xWinGc, xImage, 0, 0, 0, 0,
                      xImage->width, xImage->height);
        XDestroyImage(xImage);
        free(imageMemory);
    }
}

XtCallbackProc SelectProc(w, unused_1, unused_2)
Widget w;
caddr_t unused_1;
caddr_t unused_2;
{
    XawListReturnStruct *list_return;

    list_return = XawListShowCurrent(w);

    switch (list_return->list_index)
    {
        case ButtonQuit:
            QuitProc();
            break;
        case ButtonPreviousPage:
            PreviousProc();
            break;
        case ButtonNextPage:
            NextProc();
            break;
        default:
            fprintf(stderr, "error in SelectProc\n");
            exit(0);
    }
    XawListUnhighlight(w);
}

void QuitProc(void) { exit(0); }

void NextProc() { PageProc(ButtonNextPage); }

void PreviousProc() { PageProc(ButtonPreviousPage); }

void PageProc(direction) int direction;
{
    XEvent fake_event;
    Arg args[4];

    switch (direction)
    {
        case ButtonPreviousPage:
            if (tfDirectory > 0)
                TIFFSetDirectory(tfFile, --tfDirectory);
            else
                return;
            break;
        case ButtonNextPage:
            if (TIFFReadDirectory(tfFile) == True)
                tfDirectory++;
            else
                return;
            break;
        default:
            fprintf(stderr, "error in PageProc\n");
            exit(0);
    }

    xOffset = yOffset = 0;
    grabX = grabY = -1;

    GetTIFFHeader();
    SetNameLabel();
    GetTIFFImage();

    if (appData.usePixmap == True)
        XFreePixmap(xDisplay, xImagePixmap);
    else
        XDestroyImage(xImage);

    CreateXImage();

    /*
     * Using XtSetValues() to set the widget size causes a resize.
     * This resize gets propagated up to the parent shell.
     * In order to disable this visually disconcerting effect,
     * shell resizing is temporarily disabled.
     */
    XtSetArg(args[0], XtNallowShellResize, False);
    XtSetValues(shellWidget, args, 1);

    XtSetArg(args[0], XtNwidth, tfImageWidth);
    XtSetArg(args[1], XtNheight, tfImageHeight);
    XtSetValues(imageWidget, args, 2);

    XtSetArg(args[0], XtNallowShellResize, True);
    XtSetValues(shellWidget, args, 1);

    XClearWindow(xDisplay, XtWindow(imageWidget));

    fake_event.type = Expose;
    fake_event.xexpose.x = fake_event.xexpose.y = 0;
    fake_event.xexpose.width = tfImageWidth; /* the window will clip */
    fake_event.xexpose.height = tfImageHeight;
    EventProc(imageWidget, NULL, &fake_event);
}

void EventProc(widget, unused, event) Widget widget;
caddr_t unused;
XEvent *event;
{
    int ih, iw, ww, wh, sx, sy, w, h, dx, dy;
    Dimension w_width, w_height;
    XEvent next_event;
    Arg args[2];

    if (event->type == MappingNotify)
    {
        XRefreshKeyboardMapping((XMappingEvent *)event);
        return;
    }

    if (!XtIsRealized(widget))
        return;

    if ((event->type == ButtonPress) || (event->type == ButtonRelease))
        if (event->xbutton.button != Button1)
            return;

    iw = tfImageWidth; /* avoid sign problems */
    ih = tfImageHeight;

    /*
     * The grabX and grabY variables record where the user grabbed the image.
     * They also record whether the mouse button is down or not.
     */
    if (event->type == ButtonPress)
    {
        grabX = event->xbutton.x;
        grabY = event->xbutton.y;
        return;
    }

    /*
     * imageWidget is a Core widget and doesn't get resized.
     * So we calculate the size of its viewport here.
     */
    XtSetArg(args[0], XtNwidth, &w_width);
    XtSetArg(args[1], XtNheight, &w_height);
    XtGetValues(shellWidget, args, 2);
    ww = w_width;
    wh = w_height;
    XtGetValues(listWidget, args, 2);
    wh -= w_height;

    switch (event->type)
    {
        case Expose:
            dx = event->xexpose.x;
            dy = event->xexpose.y;
            sx = dx + xOffset;
            sy = dy + yOffset;
            w = MIN(event->xexpose.width, iw);
            h = MIN(event->xexpose.height, ih);
            break;
        case KeyPress:
            if ((grabX >= 0) || (grabY >= 0)) /* Mouse button is still down */
                return;
            switch (XLookupKeysym((XKeyEvent *)event, /* KeySyms index */ 0))
            {
                case XK_Up:
                    if (ih <
                        wh) /* Don't scroll if the window fits the image. */
                        return;
                    sy = yOffset + appData.translate;
                    sy = MIN(ih - wh, sy);
                    if (sy ==
                        yOffset) /* Filter redundant stationary refreshes. */
                        return;
                    yOffset = sy;
                    sx = xOffset;
                    dx = dy = 0;
                    w = ww;
                    h = wh;
                    break;
                case XK_Down:
                    if (ih < wh)
                        return;
                    sy = yOffset - appData.translate;
                    sy = MAX(sy, 0);
                    if (sy == yOffset)
                        return;
                    yOffset = sy;
                    sx = xOffset;
                    dx = dy = 0;
                    w = ww;
                    h = wh;
                    break;
                case XK_Left:
                    if (iw < ww)
                        return;
                    sx = xOffset + appData.translate;
                    sx = MIN(iw - ww, sx);
                    if (sx == xOffset)
                        return;
                    xOffset = sx;
                    sy = yOffset;
                    dx = dy = 0;
                    w = ww;
                    h = wh;
                    break;
                case XK_Right:
                    if (iw < ww)
                        return;
                    sx = xOffset - appData.translate;
                    sx = MAX(sx, 0);
                    if (sx == xOffset)
                        return;
                    xOffset = sx;
                    sy = yOffset;
                    dx = dy = 0;
                    w = ww;
                    h = wh;
                    break;
                default:
                    return;
            }
            break;
        case MotionNotify:
            /*
             * MotionEvent compression.  Ignore multiple motion events.
             * Ignore motion events if the mouse button is up.
             */
            if (XPending(xDisplay)) /* Xlib doesn't flush the output buffer */
                if (XtPeekEvent(&next_event))
                    if (next_event.type == MotionNotify)
                        return;
            if ((grabX < 0) || (grabY < 0))
                return;
            sx = xOffset + grabX - (int)event->xmotion.x;
            if (sx >= (iw - ww)) /* clamp x motion but allow y motion */
                sx = iw - ww;
            sx = MAX(sx, 0);
            sy = yOffset + grabY - (int)event->xmotion.y;
            if (sy >= (ih - wh)) /* clamp y motion but allow x motion */
                sy = ih - wh;
            sy = MAX(sy, 0);
            if ((sx == xOffset) && (sy == yOffset))
                return;
            dx = dy = 0;
            w = ww;
            h = wh;
            break;
        case ButtonRelease:
            xOffset = xOffset + grabX - (int)event->xbutton.x;
            xOffset = MIN(iw - ww, xOffset);
            xOffset = MAX(xOffset, 0);
            yOffset = yOffset + grabY - (int)event->xbutton.y;
            yOffset = MIN(ih - wh, yOffset);
            yOffset = MAX(yOffset, 0);
            grabX = grabY = -1;
        default:
            return;
    }

    if (appData.usePixmap == True)
    {
        if (xImageDepth == 1)
            XCopyPlane(xDisplay, xImagePixmap, XtWindow(widget), xWinGc, sx, sy,
                       w, h, dx, dy, 1);
        else
            XCopyArea(xDisplay, xImagePixmap, XtWindow(widget), xWinGc, sx, sy,
                      w, h, dx, dy);
    }
    else
        XPutImage(xDisplay, XtWindow(widget), xWinGc, xImage, sx, sy, dx, dy, w,
                  h);
}

void ResizeProc()
{
    Dimension w_width, w_height;
    int xo, yo, ww, wh;
    XEvent fake_event;
    Arg args[2];

    if ((xOffset == 0) && (yOffset == 0))
        return;

    XtSetArg(args[0], XtNwidth, &w_width);
    XtSetArg(args[1], XtNheight, &w_height);
    XtGetValues(shellWidget, args, 2);
    ww = w_width;
    wh = w_height;
    XtGetValues(listWidget, args, 2);
    wh -= w_height;

    xo = xOffset;
    yo = yOffset;

    if ((xOffset + ww) >= tfImageWidth)
        xOffset = MAX((int)tfImageWidth - ww, 0);
    if ((yOffset + wh) >= tfImageHeight)
        yOffset = MAX((int)tfImageHeight - wh, 0);

    /*
     * Send an ExposeEvent if the origin changed.
     * We have to do this because of the use and semantics of bit gravity.
     */
    if ((xo != xOffset) || (yo != yOffset))
    {
        fake_event.type = Expose;
        fake_event.xexpose.x = fake_event.xexpose.y = 0;
        fake_event.xexpose.width = tfImageWidth;
        fake_event.xexpose.height = tfImageHeight;
        EventProc(imageWidget, NULL, &fake_event);
    }
}

int XTiffErrorHandler(display, error_event)
Display *display;
XErrorEvent *error_event;
{
    char message[80];

    /*
     * Some X servers limit the size of pixmaps.
     */
    if ((error_event->error_code == BadAlloc) &&
        (error_event->request_code == X_CreatePixmap))
        fprintf(stderr, "xtiff: requested pixmap too big for display\n");
    else
    {
        XGetErrorText(display, error_event->error_code, message, 80);
        fprintf(stderr, "xtiff: error code %s\n", message);
    }

    exit(0);
}

void Usage()
{
    fprintf(stderr, "Usage xtiff: [options] tiff-file\n");
    fprintf(stderr, "\tstandard Xt options\n");
    fprintf(stderr, "\t[-help]\n");
    fprintf(stderr, "\t[-gamma gamma]\n");
    fprintf(stderr, "\t[-usePixmap (True | False)]\n");
    fprintf(stderr, "\t[-viewportWidth pixels]\n");
    fprintf(stderr, "\t[-viewportHeight pixels]\n");
    fprintf(stderr, "\t[-translate pixels]\n");
    fprintf(stderr, "\t[-verbose (True | False)]\n");
    exit(0);
}
