# Copyright: 2021-2023 Paul Obermeier (obermeier@poSoft.de)
# Distributed under BSD license.

namespace eval pawt {
    variable ns [namespace current]

    namespace ensemble create

    namespace export CreateImage
    namespace export DiffImages
    namespace export GetImageAsList
    namespace export GetImageAsMatrix
    namespace export GetImageAsPhoto
    namespace export GetImageAsPseudoColorPhoto
    namespace export GetImageByteOrder
    namespace export GetImageHeight
    namespace export GetImageFormats
    namespace export GetImageMinMax
    namespace export GetImageMeanStdDev
    namespace export GetImageMaxAsString
    namespace export GetImageMeanAsString
    namespace export GetImageMinAsString
    namespace export GetImageNumChannels
    namespace export GetImageNumPixels
    namespace export GetImagePixel
    namespace export GetImagePixelAsString
    namespace export GetImagePixelSize
    namespace export GetImagePixelType
    namespace export GetImageScanOrder
    namespace export GetImageStdDevAsString
    namespace export GetImageWidth
    namespace export HavePkg
    namespace export ReadImageFile
    namespace export ReadImageHeader
    namespace export WriteImageFile

    variable _PawtIntro {
        # PAWT - Picture Automation With Tcl

        `PAWT` is a high-level Tcl interface for handling images with byte, short, int,
        float or double pixel values.
        Currently parsers for FITS, FLIR, RAW and PPM image formats are available.

        For displaying these images the `Img` extension is needed.
        Note, that for display of 32-bit integer and 64-bit double images
        `Img` version 1.4.14 or newer is needed.

        The following table shows the functionality of the supported file formats.
        * `+` indicates that the functionality is implemented.
        * `-` indicates that the functionality is not supported by the file format.

    | Format   | Channels |        Read                           |        Write                          |
    |          |          | byte  | short |  int  | float |double | byte  | short |  int  | float |double |
    | ---------|----------|-------|-------|-------|-------|-------|-------|-------|-------|-------|-------|
    | FITS     |    1     |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |
    | FITS     |    3     |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |
    | ---------|----------|-------|-------|-------|-------|-------|-------|-------|-------|-------|-------|
    | FLIR     |    1     |   -   |   +   |   +   |   +   |   +   |   -   |   +   |   +   |   +   |   +   |
    | FLIR     |    3     |   -   |   -   |   -   |   -   |   -   |   -   |   -   |   -   |   -   |   -   |
    | ---------|----------|-------|-------|-------|-------|-------|-------|-------|-------|-------|-------|
    | PPM      |    1     |   +   |   +   |   -   |   -   |   -   |   +   |   +   |   -   |   -   |   -   |
    | PPM      |    3     |   +   |   +   |   -   |   -   |   -   |   +   |   +   |   -   |   -   |   -   |
    | ---------|----------|-------|-------|-------|-------|-------|-------|-------|-------|-------|-------|
    | RAW      |    1     |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |
    | RAW      |    3     |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |   +   |

        The following table shows the supported scan orders of the file formats.

    | ScanOrder | TopDown | BottomUp |
    | ----------|---------|----------|
    | FITS      |    -    |    +     |
    | FLIR      |    +    |    -     |
    | PPM       |    +    |    -     |
    | RAW       |    +    |    +     |


        Important links:
        * [PAWT homepage](https://www.tcl3d.org/pawt)
        * [File format documentation](https://www.tcl3d.org/pawt/download/PawtFileFormats.pdf)
    }

    variable _ruff_preamble {
        The `pawt` namespace provides commands to handle image files 
        with byte, short, int, float or double values with Tcl.

        The images are stored in a Tcl dict using 2 keys: `Header` and `Data`.
        See procedures [ReadImageFile] and [ReadImageHeader] for a description of the keys.

        `PAWT` has procedures for the following purposes:
        * Create an image and fill it with data from a list or matrix: [CreateImage].
        * Read and write images in the supported file formats:
          [ReadImageFile] [WriteImageFile] [GetImageFormats].
        * Compare two images: [DiffImages]
        * Retrieve image data as a list, matrix or Tk photo: 
          [GetImageAsList] [GetImageAsMatrix] [GetImageAsPhoto] [GetImageAsPseudoColorPhoto]
        * Retrieve header information: [ReadImageHeader] 
          [GetImageWidth] [GetImageHeight] [GetImagePixelType] [GetImagePixelSize]
          [GetImageNumChannels] [GetImageNumPixels] [GetImageScanOrder] [GetImageByteOrder]
        * Retrieve image statistics: [GetImageMinMax] [GetImageMeanStdDev]
          [GetImageMinAsString] [GetImageMaxAsString] [GetImageMeanAsString] [GetImageStdDevAsString]
        * Retrieve pixel information: [GetImagePixel] [GetImagePixelAsString]
        
        See the `PAWT` test suite for usage examples.
    }

    proc _Init {} {
        variable pkgInfo

        set retVal [catch {package require fitstcl} version]
        set pkgInfo(fitstcl,avail)   [expr !$retVal]
        set pkgInfo(fitstcl,version) $version 

        set retVal [catch {package require img::raw} version]
        set pkgInfo(Img,avail)   [expr !$retVal]
        set pkgInfo(Img,version) $version 
    }

    proc GetImageFormats {} {
        # Return the supported image formats as a list.
        #
        # Currently the following image formats are supported:
        # * `fits`
        # * `flir`
        # * `raw`
        # * `ppm`

        return [list "raw" "ppm" "flir" "fits"]
   }

    proc HavePkg { pkgName } {
        # Check, if an external package needed by PAWT is available.
        #
        # pkgName - The name of the external package.
        #           Currently supported package names: `Img`, `fitstcl`.
        #
        # Returns true, if package $pkgName was loaded successfully.
        # Otherwise returns false.

        variable pkgInfo

        if { [info exists pkgInfo($pkgName,avail)] } {
            return $pkgInfo($pkgName,avail)
        }
        return 0
    } 

    #
    # Internal utility procedures.
    #

    # PseudoColor lookup table procedures.
    # Code taken from http://dsp.stackexchange.com/questions/4677/how-to-generate-false-color-paletteI
    # Implements the following lookup, which is known as jet in Matlab.
    # 0.0  -> (  0,   0, 128)  dark blue
    # 0.25 -> (  0, 255,   0)  green
    # 0.5  -> (255, 255,   0)  yellow
    # 0.75 -> (255, 128,   0)  orange
    # 1.0  -> (255,   0,   0)  red

    proc _GetRedGradient { val } {
        if { $val <= 0.25 } {
            return 0
        } elseif { $val <= 0.5 } {
            return [expr { int ((1020.0 * $val - 255.0)) }]
        } else {
            return 255
        }
    }

    proc _GetGreenGradient { val } {
        if { $val <= 0.25 } {
            return [expr { int (1020.0 * $val) }]
        } elseif { $val <= 0.5 } {
            set retVal 255
        } else {
            return [expr { int (-510.0 * $val + 510.0) }] 
        }
    }

    proc _GetBlueGradient { val } {
        if { $val <= 0.25 } {
            return [expr { int (-512.0 * $val + 128.0) }]
        } else {
            return 0
        }
    }

    proc _GetPixelSize { pixelType } {
        if { $pixelType eq "float" } {
            return 4
        } elseif { $pixelType eq "double" } {
            return 8
        } elseif { $pixelType eq "int" } {
            return 4
        } elseif { $pixelType eq "short" } {
            return 2
        } elseif { $pixelType eq "byte" } {
            return 1
        } else {
            error "Invalid PixelType value: $pixelType (must be byte, short, int, float or double)"
        }
    }

    proc _GetScanFormat { pixelType byteOrder } {
        if { $pixelType eq "float" } {
            if { $byteOrder eq "Intel" } {
                set scanFmt "r"
            } else {
                set scanFmt "R"
            }
        } elseif { $pixelType eq "double" } {
            if { $byteOrder eq "Intel" } {
                set scanFmt "q"
            } else {
                set scanFmt "Q"
            }
        } elseif { $pixelType eq "int" } {
            if { $byteOrder eq "Intel" } {
                set scanFmt "iu"
            } else {
                set scanFmt "Iu"
            }
        } elseif { $pixelType eq "short" } {
            if { $byteOrder eq "Intel" } {
                set scanFmt "su"
            } else {
                set scanFmt "Su"
            }
        } elseif { $pixelType eq "byte" } {
            set scanFmt "cu"
        } else {
            error "Invalid PixelType value: $pixelType (must be byte, short, int, float or double)"
        }
        return $scanFmt
    }

    proc _FormatValue { val pixelType { precision 4 } } {
        set str ""
        if { $pixelType eq "float" || $pixelType eq "double" } {
            append str [format "%.${precision}f" $val]
        } elseif { $pixelType eq "int" } {
            append str [format "%10u" $val]
        } elseif { $pixelType eq "short" } {
            append str [format "%5u" $val]
        } else {
            append str [format "%3u" $val]
        }
        return [string trim $str]
    }

    proc _Square { x } {
        return [expr {$x * $x}]
    }

    proc _Abs { x } {
        if { $x < 0 } {
            return [expr {-$x}]
        } else {
            return $x
        }
    }

    proc CreateImage { args } {
        # Create a new image dictionary.
        #
        # args - Options described below.
        #
        # -width <int>        - Image width in pixel. Default: `10`.
        # -height <int>       - Image height in pixel. Default: `10`.
        # -numchan <int>      - Number of channels contained in image. Default: `1`.
        # -byteorder <string> - `Motorola` for big-endian, `Intel` for little-endian architecture. Default: `Intel`.
        # -scanorder <string> - `TopDown` or `BottomUp`. Default: `TopDown`.
        # -pixeltype <string> - `byte`   for 1-byte unsigned integers,
        #                       `short`  for 2-byte unsigned integers,
        #                       `int`    for 4-byte unsigned integers,
        #                       `float`  for 4-byte single precision floating point values.
        #                       `double` for 8-byte double precision floating point values.
        # -fill <int|float>   - Fill value of all pixels. Default: `0`.
        # -list <list>        - List of pixel values according to setting of options
        #                       `-numchan`, `-scanorder` and `-pixeltype`.
        # -matrix <matrix>    - Matrix of pixel values according to setting of options
        #                       `-numchan`, `-scanorder` and `-pixeltype`.
        #
        # If more than one of the pixel data options `-fill`, `-list` and `-matrix` are specified,
        # `-matrix` takes precedence over `-list`, which itself takes precedence over `-fill`.
        #
        # Specifying pixel data with `-list` is faster than using `-matrix`.
        #
        # See [GetImageAsMatrix] for the definition of an image matrix.
        #
        # Returns the new image dictionary. If invalid option values are specified, an error is thrown.
        #
        # See also: ReadImageHeader ReadImageFile WriteImageFile

        set opts [dict create \
            -width      10 \
            -height     10 \
            -numchan    1 \
            -byteorder  "Intel" \
            -scanorder  "TopDown" \
            -pixeltype  "short" \
            -fill       0 \
            -list       [list] \
            -matrix     [list] \
        ]

        foreach { key value } $args {
            if { [dict exists $opts $key] } {
                if { $value eq "" } {
                    error "CreateImage: No value specified for key \"$key\"."
                }
                dict set opts $key $value
            } else {
                error "CreateImage: Unknown option \"$key\" specified."
            }
        }

        set width     [dict get $opts "-width"]
        set height    [dict get $opts "-height"]
        set numChan   [dict get $opts "-numchan"]
        set byteOrder [dict get $opts "-byteorder"]
        set scanOrder [dict get $opts "-scanorder"]
        set pixelType [dict get $opts "-pixeltype"]
        set numPixel  [expr { $width * $height }]
        set numValues [expr {$numPixel * $numChan }]

        if { $width <= 0 } {
            error "CreateImage: Width must be greater than zero. Have $width"
        }
        if { $height <= 0 } {
            error "CreateImage: Height must be greater than zero. Have $height"
        }
        if { $numChan != 1 && $numChan != 3 } {
            error "CreateImage: Number of channels must be 1 or 3. Have $numChan"
        }
        if { $byteOrder ne "Intel" && $byteOrder ne "Motorola" } {
            error "CreateImage: ByteOrder must be \"Intel\" or \"Motorola\". Have \"$byteOrder\""
        }
        if { $scanOrder ne "TopDown" && $scanOrder ne "BottomUp" } {
            error "CreateImage: ScanOrder must be \"TopDown\" or \"BottomUp\". Have \"$scanOrder\""
        }
        if { $pixelType ne "byte" && $pixelType ne "short" && $pixelType ne "int" && \
             $pixelType ne "float" && $pixelType ne "double" } {
            error "CreateImage: PixelType must be \"byte\", \"short\", \"int\", \"float\" or \"double\". Have \"$pixelType\""
        }

        set imgDict    [dict create]
        set headerDict [dict create]

        dict set headerDict Magic     ""
        dict set headerDict Width     $width
        dict set headerDict Height    $height
        dict set headerDict NumChan   $numChan
        dict set headerDict ByteOrder $byteOrder
        dict set headerDict ScanOrder $scanOrder
        dict set headerDict PixelType $pixelType
        dict set headerDict FileName  "Create-$width-$height"

        set pixelSize [pawt::_GetPixelSize   $pixelType]
        set scanFmt   [pawt::_GetScanFormat  $pixelType $byteOrder]
        dict set headerDict PixelSize  $pixelSize
        dict set headerDict ScanFormat $scanFmt
        dict set headerDict NumPixel   $numPixel
        dict set headerDict NumByte    [expr { $numPixel * $numChan * $pixelSize }]

        dict set imgDict Header $headerDict

        set listLen   [llength [dict get $opts "-list"]]
        set matrixLen [llength [dict get $opts "-matrix"]]
        if { $matrixLen > 0 } {
            if { $matrixLen != $height } {
                error "CreateImage: Number of rows in matrix ($matrixLen) does not fit image height ($height)."
            }
            set matrixWidth [llength [lindex [dict get $opts "-matrix"] 0]]
            if { $matrixWidth != $width * $numChan } {
                error "CreateImage: Number of columns in matrix ($matrixWidth) does not fit image width (${width}x${numChan})."
            }
            dict append imgDict Data [binary format "${scanFmt}$numValues" [concat {*}[dict get $opts "-matrix"]]]
        } elseif { $listLen > 0 } {
            if { $listLen != $numValues } {
                error "CreateImage: Pixel list length ($listLen) does not fit image size (${width}x${height}x${numChan})."
            }
            dict append imgDict Data [binary format "${scanFmt}$numValues" [dict get $opts "-list"]]
        } else {
            dict append imgDict Data [binary format "${scanFmt}$numValues" [lrepeat $numValues [dict get $opts "-fill"]]]
        }

        return $imgDict
    }

    proc ReadImageHeader { imgFile args } {
        # Read the header of a supported image file.
        #
        # imgFile - File name of the image.
        # args    - Optional format specific arguments. 
        #
        # The header information is stored in a dictionary containing at least the following keys:
        #
        # Magic      - Identification string of image file format.
        #              Currently supported format strings: ``FITS`, `FLIR`, `PPM`, `RAW`.
        # Width      - Width of the image in pixel (integer).
        # Height     - Height of the image in pixel (integer).
        # NumChan    - Number of channels contained in image (integer).
        # ByteOrder  - `Motorola` for big-endian, `Intel` for little-endian architecture.
        # ScanOrder  - `TopDown` or `BottomUp`
        # PixelType  - `byte`   for 1-byte unsigned integers,
        #              `short`  for 2-byte unsigned integers,
        #              `int`    for 4-byte unsigned integers,
        #              `float`  for 4-byte single precision floating point values.
        #              `double` for 8-byte double precision floating point values.
        # PixelSize  - Pixel size in bytes: 1, 2, 4 or 8.
        # ScanFormat - Format string for Tcl `binary` command to read image data.
        # NumPixel   - Number of pixels contained in image (integer).
        # NumByte    - Number of bytes contained in image data (integer).
        #
        # See format specific `ReadImageHeader` procedures for optional format
        # specific arguments and additional dictionary keys:
        # * [::pawt::fits::ReadImageHeader] 
        # * [::pawt::flir::ReadImageHeader]
        # * [::pawt::ppm::ReadImageHeader]
        # * [::pawt::raw::ReadImageHeader]
        #
        # Returns the header information as a dictionary.
        # If no supported image format header can be detected, an empty dictionary is returned.
        #
        # See also: ReadImageFile WriteImageFile 

        foreach fmt [pawt::GetImageFormats] {
            set retVal [catch { pawt::${fmt}::ReadImageHeader $imgFile {*}$args } headerDict]
            if { $retVal == 0 } {
                return $headerDict
            }
        }
        return [dict create]
    }

    proc ReadImageFile { imgFile args } {
        # Read a supported image file into a dictionary.
        #
        # imgFile - File name of the image.
        # args    - Optional format specific arguments. 
        #
        # Return the image data as a dictionary containing 2 keys: `Header` and `Data`.
        # * Key `Data` contains the raw image data information as a binary string.
        # * Key `Header` contains additional information about the image and is itself a dictionary.
        #
        # See format specific `ReadImageFile` procedures for optional format
        # specific arguments and additional dictionary keys:
        # * [::pawt::fits::ReadImageFile] 
        # * [::pawt::flir::ReadImageFile]
        # * [::pawt::ppm::ReadImageFile]
        # * [::pawt::raw::ReadImageFile]
        #
        # See [ReadImageHeader] for a description of the header dictionary.
        #
        # The following utility procedures can be used to get the header dictionary information:
        # * [GetImageWidth]
        # * [GetImageHeight]
        # * [GetImagePixelType]
        # * [GetImagePixelSize]
        # * [GetImageNumChannels]
        # * [GetImageNumPixels]
        # * [GetImageScanOrder]
        # * [GetImageByteOrder]
        #
        # Returns the image information as a dictionary.
        # If no supported image format can be detected, an error is thrown.
        #
        # See also: ReadImageHeader WriteImageFile
        
        set headerDict [pawt::ReadImageHeader $imgFile {*}$args]
        if { [dict size $headerDict] == 0 } {
            error "Cannot read file $imgFile"
        }
        set fmt [string tolower [dict get $headerDict Magic]]
        return [pawt::${fmt}::ReadImageFile $imgFile {*}$args]
    }

    proc WriteImageFile { imgDict imgFile fmt args } {
        # Write an image dictionary into a supported image file.
        #
        # imgDict - Image dictionary.
        # imgFile - File name of the image.
        # fmt     - Image file format. See [GetImageFormats] for a list of supported formats.
        # args    - Optional format specific arguments. 
        #
        # See format specific `WriteImageFile` procedures for optional format
        # specific arguments:
        # * [::pawt::fits::WriteImageFile] 
        # * [::pawt::flir::WriteImageFile]
        # * [::pawt::ppm::WriteImageFile]
        # * [::pawt::raw::WriteImageFile]
        #
        # See also: ReadImageHeader ReadImageFile GetImageFormats
        
        if { [lsearch -nocase [pawt::GetImageFormats] $fmt] < 0 } {
            error "Unsupported image format $fmt"
        }
        pawt::${fmt}::WriteImageFile $imgDict $imgFile {*}$args
    }

    #
    # Image data procedures.
    #

    proc GetImageAsList { imgDict args } {
        # Get the image data as a list.
        #
        # imgDict - Image dictionary.
        # args    - Options described below.
        #
        # -scanorder <string> - Get the image data in `TopDown`or `BottomUp` scan order.
        #                       If option is not specified, the default value is the scan order
        #                       as specified in the image dict.
        #
        # Returns the image data as a Tcl list.
        # If invalid option values are given, an error is thrown.
        #
        # See also: GetImageAsMatrix GetImageAsPhoto GetImageAsPseudoColorPhoto

        upvar 1 $imgDict myDict

        set opts [dict create \
            -scanorder  "" \
        ]

        foreach { key value } $args {
            if { [dict exists $opts $key] } {
                if { $value eq "" } {
                    error "GetImageAsList: No value specified for key \"$key\"."
                }
                dict set opts $key $value
            } else {
                error "GetImageAsList: Unknown option \"$key\" specified."
            }
        }

        set headerDict [dict get $myDict Header]

        set numPixel  [dict get $headerDict NumPixel]
        set numChan   [dict get $headerDict NumChan]
        set scanFmt   [dict get $headerDict ScanFormat]
        set scanOrder [dict get $headerDict ScanOrder]
        set fileName  [dict get $headerDict FileName]

        set outputScanOrder [dict get $opts "-scanorder"]
        if { $outputScanOrder eq "" } {
            set outputScanOrder $scanOrder
        }
        if { $scanOrder eq $outputScanOrder } {
            set numValues [expr {$numPixel * $numChan }]
            set retVal [binary scan [dict get $myDict Data] "${scanFmt}${numValues}" valList]
            if { $retVal != 1 } {
                error "Could not scan pixels of image $fileName"
            }
        } else {
            set pixelSize [dict get $headerDict PixelSize]
            set width     [dict get $headerDict Width]
            set height    [dict get $headerDict Height]

            set numBytesPerRow [expr { $width * $pixelSize * $numChan }]
            set numValues [expr { $width * $numChan }]

            set pos [expr { $width * ($height - 1) * $pixelSize * $numChan } ]
            for { set row 0 } { $row < $height } { incr row } {
                set fmtStr "@${pos}${scanFmt}${numValues}"
                set retVal [binary scan [dict get $myDict Data] $fmtStr rowList]
                if { $retVal != 1 } {
                    error "Could not scan row $row of image $fileName"
                }
                append valList $rowList
                append valList " "
                incr pos [expr { -$numBytesPerRow }]
            }
        }
        return $valList
    }

    proc GetImageAsMatrix { imgDict args } {
        # Get the image data as a matrix.
        #
        # imgDict - Image dictionary.
        # args    - Options described below.
        #
        # -scanorder <string> - Get the image data in `TopDown`or `BottomUp` scan order.
        #                       If option is not specified, the default value is the scan order
        #                       as specified in the image dict.
        #
        # The matrix data is stored as a list of lists. Each sub-list contains
        # the values for the row values.
        # The main (outer) list contains the rows of the matrix.
        # Example:
        #     { { R1_C1 R1_C2 R1_C3 } { R2_C1 R2_C2 R2_C3 } }
        #
        # Returns the image data as a matrix.
        # If invalid option values are given, an error is thrown.
        #
        # See also: GetImageAsList GetImageAsPhoto GetImageAsPseudoColorPhoto

        upvar 1 $imgDict myDict

        set opts [dict create \
            -scanorder  "" \
        ]

        foreach { key value } $args {
            if { [dict exists $opts $key] } {
                if { $value eq "" } {
                    error "GetImageAsMatrix: No value specified for key \"$key\"."
                }
                dict set opts $key $value
            } else {
                error "GetImageAsMatrix: Unknown option \"$key\" specified."
            }
        }

        set headerDict [dict get $myDict Header]

        set numPixel  [dict get $headerDict NumPixel]
        set numChan   [dict get $headerDict NumChan]
        set scanFmt   [dict get $headerDict ScanFormat]
        set scanOrder [dict get $headerDict ScanOrder]
        set fileName  [dict get $headerDict FileName]

        set pixelSize  [dict get $headerDict PixelSize]
        set width      [dict get $headerDict Width]
        set height     [dict get $headerDict Height]

        set numBytesPerRow [expr { $width * $pixelSize * $numChan }]
        set numValues [expr { $width * $numChan }]

        set pos 0
        for { set row 0 } { $row < $height } { incr row } {
            set fmtStr "@${pos}${scanFmt}${numValues}"
            set retVal [binary scan [dict get $myDict Data] $fmtStr rowList]
            if { $retVal != 1 } {
                error "Could not scan pixels of image $fileName"
            }
            set rowLists($row) $rowList
            incr pos $numBytesPerRow
        }

        set outputScanOrder [dict get $opts "-scanorder"]
        if { $outputScanOrder eq "" } {
            set outputScanOrder $scanOrder
        }

        if { $scanOrder eq $outputScanOrder } {
            for { set row 0 } { $row < $height } { incr row } {
                lappend matrixList $rowLists($row)
            }
        } else {
            for { set row [expr {$height-1}] } { $row >= 0 } { incr row -1 } {
                lappend matrixList $rowLists($row)
            }
        }
        return $matrixList
    }

    proc GetImageAsPhoto { imgDict args } {
        # Get the image data as a Tk photo image.
        #
        # imgDict - Image dictionary.
        # args    - Options described below.
        #
        # -min <int|float>   - Minimum pixel value to be used for mapping the input data to 8-bit 
        #                      photo image values. Default is the minimum value of the image data.
        # -max <int|float>   - Maximum pixel value to be used for mapping the input data to 8-bit 
        #                      photo image values. Default is the maximum value of the image data.
        # -gamma <int|float> - Gamma correction when mapping the input data to 8-bit photo image
        #                      values. Default: 1.0.
        #
        # Note: This procedure needs the `img::raw` extension.
        #
        # Returns the image data as a Tk photo image. 
        # If invalid option values are given, an error is thrown.
        #
        # See also: GetImageAsList GetImageAsMatrix GetImageAsPseudoColorPhoto

        upvar 1 $imgDict myDict

        if { ! [pawt::HavePkg "Img"] } {
            error "GetImageAsPhoto: Missing img::raw extension"
        }

       set opts [dict create \
            -min   "" \
            -max   "" \
            -gamma "" \
        ]

        foreach { key value } $args {
            if { [dict exists $opts $key] } {
                if { $value eq "" } {
                    error "GetImageAsPhoto: No value specified for key \"$key\"."
                }
                dict set opts $key $value
            } else {
                error "GetImageAsPhoto: Unknown option \"$key\" specified."
            }
        }

        set headerDict [dict get $myDict Header]

        set width     [dict get $headerDict Width]
        set height    [dict get $headerDict Height]
        set numChan   [dict get $headerDict NumChan]

        set byteOrder [dict get $headerDict ByteOrder]
        set pixelType [dict get $headerDict PixelType]
        set scanOrder [dict get $headerDict ScanOrder]

        set fmtStr "RAW -useheader 0 -uuencode 0 "
        append fmtStr "-width $width "
        append fmtStr "-height $height "
        append fmtStr "-nchan $numChan "
        append fmtStr "-byteorder $byteOrder "
        append fmtStr "-pixeltype $pixelType "
        append fmtStr "-scanorder $scanOrder "
        if { [dict get $opts -min] ne "" } {
            append fmtStr "-min [dict get $opts -min] "
        }
        if { [dict get $opts -max] ne "" } {
            append fmtStr "-max [dict get $opts -max] "
        }
        if { [dict get $opts -gamma] ne "" } {
            append fmtStr "-gamma [dict get $opts -gamma] "
        }
        return [image create photo -data [dict get $myDict Data] -format $fmtStr]
    }

    proc DiffImages { imgDict1 imgDict2 args } {
        # Compare two image dictionaries.
        #
        # imgDict1 - First image dictionary.
        # imgDict2 - Second image dictionary.
        # args     - Options described below.
        #
        # -threshold <int|float> - Threshold value for comparing pixel values. Default: `0`.
        # -photo <string>        - Tk photo image where the differing pixels are marked. Default: `""`.
        # -markcolor <string>    - Color to mark differing pixels. Default: `#FF00FF`.
        #
        # If no `-photo` option is given or the empty string is specified, only the number of differing pixel is returned.
        #
        # The procedure can be used as a coroutine.
        #
        # Returns the number of differing pixels.
        # If the sizes of the two image dictionaries are not equal, an error is thrown.
        # If the specified photo image does not fit the size of the image dictionaries, an error is thrown.
        #
        # See also: ReadImageFile GetImageAsPhoto

        upvar 1 $imgDict1 myDict1
        upvar 1 $imgDict2 myDict2

        set opts [dict create \
            -threshold 0 \
            -photo     "" \
            -markcolor "#FF00FF" \
        ]

        foreach { key value } $args {
            if { [dict exists $opts $key] } {
                if { $value eq "" } {
                    error "DiffImages: No value specified for key \"$key\"."
                }
                dict set opts $key $value
            } else {
                error "DiffImages: Unknown option \"$key\" specified."
            }
        }

        set headerDict1 [dict get $myDict1 Header]
        set headerDict2 [dict get $myDict2 Header]

        set width1     [dict get $headerDict1 Width]
        set height1    [dict get $headerDict1 Height]
        set pixelSize1 [dict get $headerDict1 PixelSize]
        set numChan1   [dict get $headerDict1 NumChan]
        set scanOrder1 [dict get $headerDict1 ScanOrder]

        set width2     [dict get $headerDict2 Width]
        set height2    [dict get $headerDict2 Height]
        set pixelSize2 [dict get $headerDict2 PixelSize]
        set numChan2   [dict get $headerDict2 NumChan]
        set scanOrder2 [dict get $headerDict2 ScanOrder]

        if { $width1 != $width2 || $height1 != $height2 } {
            error "Images differ in size. Difference image calculation not possible."
        }
        if { $pixelSize1 != $pixelSize2 } {
            error "Images differ in pixel size. Difference image calculation not possible."
        }
        if { $numChan1 != $numChan2 } {
            error "Images differ in number of channels. Difference image calculation not possible."
        }
        if { ! ($numChan1 == 1 || $numChan1 == 3) } {
            error "Difference image calculation only possible for 1 or 3-channel images."
        }

        set threshold [dict get $opts -threshold]
        set photo     [dict get $opts -photo]
        set markColor [dict get $opts -markcolor]

        if { $photo ne "" } {
            if { $width1 != [image width $photo] || $height1 != [image height $photo] } {
                error "Photo image size is different from image dictionary size. Difference image calculation not possible."
            }
        }

        set numDifferent 0
        if { [info coroutine] ne "" } {
            yield [list $numDifferent 0]
        }

        # If the scan order of both images are equal, we can use them as is.
        # Otherwise we converted the scan order of the second image to the order
        # of the first image.
        set imgMatrix1 [GetImageAsMatrix myDict1]
        if { $scanOrder1 eq $scanOrder2 } {
            set imgMatrix2 [GetImageAsMatrix myDict2]
        } else {
            set imgMatrix2 [GetImageAsMatrix myDict2 -scanorder $scanOrder1]
        }

        for { set row 0 } { $row < $height1 } { incr row } {
            set rowList1 [lindex $imgMatrix1 $row]
            set rowList2 [lindex $imgMatrix2 $row]
            if { $numChan1 == 1 } {
                for { set col 0 } { $col < $width1 } { incr col } {
                    set pixelVal1 [lindex $rowList1 $col]
                    set pixelVal2 [lindex $rowList2 $col]
                    if { [pawt::_Abs [expr {$pixelVal1 - $pixelVal2}]] > $threshold } {
                        incr numDifferent
                        if { $photo ne "" } {
                            $photo put $markColor -to $col $row
                        }
                    }
                }
            } elseif { $numChan1 == 3 } {
                for { set col 0 } { $col < $width1 } { incr col } {
                    set startIndex [expr { $col * 3 }]
                    set endIndex   [expr { $startIndex + 2 }]
                    set pixelVal1 [lrange $rowList1 $startIndex $endIndex]
                    set pixelVal2 [lrange $rowList2 $startIndex $endIndex]
                    lassign $pixelVal1 r1 g1 b1
                    lassign $pixelVal2 r2 g2 b2
                    if { [pawt::_Abs [expr {$r1 - $r2}]] > $threshold || \
                         [pawt::_Abs [expr {$g1 - $g2}]] > $threshold || \
                         [pawt::_Abs [expr {$b1 - $b2}]] > $threshold } {
                        incr numDifferent
                        if { $photo ne "" } {
                            $photo put $markColor -to $col $row
                        }
                    }
                }
            }
            if { [info coroutine] ne "" } {
                yield [list $numDifferent [expr { int (100.0 * $row / $height1) }]]
            }
        }
        if { [info coroutine] ne "" } {
            yield [list $numDifferent 100]
        }
        return $numDifferent
    }

    proc GetImageAsPseudoColorPhoto { imgDict args } {
        # Get the image data as a pseudo-color Tk photo image.
        #
        # imgDict - Image dictionary.
        # args    - Options described below.
        #
        # -mode <string> - Pseudo-color mapping mode.
        #                  Possible values: `pseudo`, `redgreen`.
        #                  Default: `pseudo`.
        #
        # Note:
        # * Procedure needs the `img::raw` extension.
        # * Functionality currently valid only for 1-channel 16-bit images.
        #
        # Returns the image data as a pseudo-color Tk photo image.
        # If invalid option values are given, an error is thrown.
        #
        # See also: GetImageAsList GetImageAsMatrix GetImageAsPhoto

        upvar 1 $imgDict myDict

        if { ! [pawt::HavePkg "Img"] } {
            error "GetImageAsPseudoColorPhoto: Missing img::raw extension"
        }

        set opts [dict create \
            -mode "pseudo" \
        ]

        foreach { key value } $args {
            if { [dict exists $opts $key] } {
                if { $value eq "" } {
                    error "GetImageAsPseudoColorPhoto: No value specified for key \"$key\"."
                }
                dict set opts $key $value
            } else {
                error "GetImageAsPseudoColorPhoto: Unknown option \"$key\" specified."
            }
        }

        set headerDict [dict get $myDict Header]

        set numPixel   [dict get $headerDict NumPixel]
        set scanFmt    [dict get $headerDict ScanFormat]
        set pixelSize  [dict get $headerDict PixelSize]
        set fileName   [dict get $headerDict FileName]
        set width      [dict get $headerDict Width]
        set height     [dict get $headerDict Height]
        set scanOrder  [dict get $headerDict ScanOrder]
        set numChan    [dict get $headerDict NumChan]

        if { ! ($numChan == 1 && $pixelSize == 2) } {
            error "GetImageAsPseudoColorPhoto: Only possible for 16-bit images with 1 channel."
        }
        set phImg [image create photo -width $width -height $height]
        set retVal [binary scan [dict get $myDict Data] "${scanFmt}${numPixel}" valList]
        if { $retVal != 1 } {
            error "GetImageAsPseudoColorPhoto: Could not scan pixels of image $fileName"
        }
        set mode [dict get $opts -mode]

        set index 0
        for { set y 0 } { $y < $height } { incr y } {
            if { $scanOrder eq "TopDown" } {
                set photoY $y
            } else {
                set photoY [expr {$height - $y - 1}]
            }
            set rowList [list]
            if { $mode eq "pseudo" } {
                for { set x 0 } { $x < $width } { incr x } {
                    set pixelVal [expr { [lindex $valList $index] / 65535.0 }]
                    set r [pawt::_GetRedGradient   $pixelVal]
                    set g [pawt::_GetGreenGradient $pixelVal]
                    set b [pawt::_GetBlueGradient  $pixelVal]
                    lappend rowList $r $g $b
                    incr index
                }
            } elseif { $mode eq "redgreen" } {
                for { set x 0 } { $x < $width } { incr x } {
                    set pixelVal [lindex $valList $index]
                    set r [expr {($pixelVal     ) & 0xFF}]
                    set g [expr {($pixelVal >> 8) & 0xFF}]
                    lappend rowList $r $g 0
                    incr index
                }
            } else {
                error "GetImageAsPseudoColorPhoto: Invalid mode $mode."
            }
            $phImg put [binary format "cu*" $rowList] -to 0 $photoY \
                   -format "RAW -useheader 0 -uuencode 0 -width $width -height 1 -nchan 3 -pixeltype byte -scanorder $scanOrder"
        }
        return $phImg
    }

    proc GetImageMinMax { imgDict } {
        # Calculate the minimum and maximum pixel values of an image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # Side effect:
        # The calculated minimum and maximum values are stored in the header dictionary
        # for use by procedures [GetImageMinAsString] and [GetImageMaxAsString].
        #
        # Returns the minimum and maximum pixel values as a list.
        #
        # See also: GetImageMeanStdDev

        upvar 1 $imgDict myDict

        set numPixel  [dict get $myDict Header NumPixel]
        set scanFmt   [dict get $myDict Header ScanFormat]
        set pixelType [dict get $myDict Header PixelType]
        set fileName  [dict get $myDict Header FileName]
        set numChan   [dict get $myDict Header NumChan]

        set numValues [expr {$numPixel * $numChan }]

        set retVal [binary scan [dict get $myDict Data] "${scanFmt}${numValues}" valList]
        if { $retVal != 1 } {
            error "Could not scan pixels of image $fileName"
        }
        if { $pixelType eq "float" || $pixelType eq "double" } {
            set sortedList [lsort -increasing -real $valList]
        } else {
            set sortedList [lsort -increasing -integer $valList]
        }
        set minVal [lindex $sortedList 0]
        set maxVal [lindex $sortedList end]
        dict set myDict Header MinValue $minVal
        dict set myDict Header MaxValue $maxVal
        return [list $minVal $maxVal]
    }

    proc GetImageMeanStdDev { imgDict } {
        # Calculate the mean and standard deviation values of an image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # Side effect:
        # The calculated mean and standard deviation values are stored in the header dictionary
        # for use by procedures [GetImageMeanAsString] and [GetImageStdDevAsString].
        #
        # Returns the mean and standard deviation values as a list.
        #
        # See also: GetImageMinMax

        upvar 1 $imgDict myDict

        set numPixel  [dict get $myDict Header NumPixel]
        set scanFmt   [dict get $myDict Header ScanFormat]
        set pixelType [dict get $myDict Header PixelType]
        set fileName  [dict get $myDict Header FileName]
        set numChan   [dict get $myDict Header NumChan]

        set numValues [expr {$numPixel * $numChan }]
        set sum 0
        set std 0.0

        set retVal [binary scan [dict get $myDict Data] "${scanFmt}${numValues}" valList]
        if { $retVal != 1 } {
            error "Could not scan pixels of image $fileName"
        }
        # Calculate mean value.
        foreach val $valList {
            set sum [expr {$sum + $val}]
        }
        set meanVal [expr {double($sum) / double($numValues)}]

        # Calculate standard deviation.
        foreach val $valList {
            set diff [pawt::_Square [expr {$meanVal - $val}]]
            set std  [expr { $std + $diff}]
        }
        set stdDev [expr {sqrt (double($std) / double($numValues-1))}]

        dict set myDict Header MeanValue $meanVal
        dict set myDict Header StdDev    $stdDev
        return [list $meanVal $stdDev]
    }

    proc GetImagePixel { imgDict x y } {
        # Get the pixel value at a specific image position.
        #
        # imgDict - Image dictionary.
        # x       - Horizontal image position.
        # y       - Vertical image position.
        #
        # The `y` value is interpreted according to the setting of the `ScanOrder` header 
        # dictionary key.
        #
        # Returns the pixel value as a single value in the case of a 1-channel image and as
        # a list of 3 values in the case of a 3-channel image.
        #
        # See also: GetImagePixelAsString GetImageMinAsString GetImageMaxAsString

        upvar 1 $imgDict myDict

        set width     [dict get $myDict Header Width]
        set height    [dict get $myDict Header Height]
        set scanFmt   [dict get $myDict Header ScanFormat]
        set scanOrder [dict get $myDict Header ScanOrder]
        set pixelSize [dict get $myDict Header PixelSize]
        set numChan   [dict get $myDict Header NumChan]
        set valList [list]
        if { $x >= 0 && $y >= 0 && $x < $width && $y < $height } {
            if { $scanOrder eq "BottomUp" } {
                set posxy [expr {$x + ($height - $y - 1) * $width}]
            } else {
                set posxy [expr {$x + $y * $width}]
            }
            set pos [expr {$posxy * $pixelSize * $numChan}]

            set fmtStr "@${pos}${scanFmt}${numChan}"
            set retVal [binary scan [dict get $myDict Data] $fmtStr valList]
            if { $retVal != 1 } {
                error "Could not read pixel value from byte position $pos"
            }
        }
        return $valList
    }

    proc GetImagePixelAsString { imgDict x y { precision 4 } } {
        # Get the pixel value at a specific image position.
        #
        # imgDict   - Image dictionary.
        # x         - Horizontal image position.
        # y         - Vertical image position.
        # precision - Precision of floating point values, i.e. number of digits after the decimal point.
        #
        # The `y` value is interpreted according to the setting of the `ScanOrder` header 
        # dictionary key.
        #
        # Returns the pixel value as string.
        #
        # See also: GetImagePixelAsString GetImageMinAsString GetImageMaxAsString

        upvar 1 $imgDict myDict

        set valList [GetImagePixel myDict $x $y]
        set str ""
        foreach val $valList {
            set retVal [catch { pawt::_FormatValue $val [dict get $myDict Header PixelType] $precision } valStr ]
            if { $retVal != 0 } {
                return "N/A"
            }
            append str $valStr " "
        }
        return [string trim $str]
    }

    proc GetImageMinAsString { imgDict { precision 4 } } {
        # Return the minimum pixel value of the image dictionary.
        #
        # imgDict   - Image dictionary.
        # precision - Precision of floating point values, i.e. number of digits after the decimal point.
        #
        # Returns the minimum pixel value of the image dictionary as string.
        # If [GetImageMinMax] has not been called before, the string `N/A` is returned.
        #
        # See also: GetImageMaxAsString GetImageMinMax GetImageMeanStdDev

        upvar 1 $imgDict myDict

        if { ! [dict exists $myDict Header MinValue] } {
            return "N/A"
        }

        return [pawt::_FormatValue [dict get $myDict Header MinValue] [dict get $myDict Header PixelType] $precision]
    }

    proc GetImageMaxAsString { imgDict { precision 4 } } {
        # Return the maximum pixel value of the image dictionary.
        #
        # imgDict   - Image dictionary.
        # precision - Precision of floating point values, i.e. number of digits after the decimal point.
        #
        # Returns the maximum pixel value of the image dictionary as string.
        # If [GetImageMinMax] has not been called before, the string `N/A` is returned.
        #
        # See also: GetImageMinAsString GetImageMinMax GetImageMeanStdDev

        upvar 1 $imgDict myDict

        if { ! [dict exists $myDict Header MaxValue] } {
            return "N/A"
        }

        return [pawt::_FormatValue [dict get $myDict Header MaxValue] [dict get $myDict Header PixelType] $precision]
    }

    proc GetImageMeanAsString { imgDict { precision 4 } } {
        # Return the mean pixel value of the image dictionary.
        #
        # imgDict   - Image dictionary.
        # precision - Precision of floating point values, i.e. number of digits after the decimal point.
        #
        # Returns the mean pixel value of the image dictionary as string.
        # If [GetImageMeanStdDev] has not been called before, the string `N/A` is returned.
        #
        # See also: GetImageStdDevAsString GetImageMinMax GetImageMeanStdDev

        upvar 1 $imgDict myDict

        if { ! [dict exists $myDict Header MeanValue] } {
            return "N/A"
        }

        return [format "%.${precision}f" [dict get $myDict Header MeanValue] $precision]
    }

    proc GetImageStdDevAsString { imgDict { precision 4 } } {
        # Return the standard deviation pixel value of the image dictionary.
        #
        # imgDict   - Image dictionary.
        # precision - Precision of floating point values, i.e. number of digits after the decimal point.
        #
        # Returns the standard deviation pixel value of the image dictionary as string.
        # If [GetImageMeanStdDev] has not been called before, the string `N/A` is returned.
        #
        # See also: GetImageMeanAsString GetImageMinMax GetImageMeanStdDev

        upvar 1 $imgDict myDict

        if { ! [dict exists $myDict Header StdDev] } {
            return "N/A"
        }

        return [format "%.${precision}f" [dict get $myDict Header StdDev] $precision]
    }

    proc GetImageWidth { imgDict } {
        # Returns key `Width` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageHeight GetImagePixelType GetImagePixelSize GetImageNumChannels GetImageNumPixels GetImageScanOrder GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header Width]
    }

    proc GetImageHeight { imgDict } {
        # Returns key `Height` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImagePixelType GetImagePixelSize GetImageNumChannels GetImageNumPixels GetImageScanOrder GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header Height]
    }

    proc GetImagePixelType { imgDict } {
        # Returns key `PixelType` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImageHeight GetImagePixelSize GetImageNumChannels GetImageNumPixels GetImageScanOrder GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header PixelType]
    }

    proc GetImagePixelSize { imgDict } {
        # Returns key `PixelSize` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImageHeight GetImagePixelType GetImageNumChannels GetImageNumPixels GetImageScanOrder GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header PixelSize]
    }

    proc GetImageNumChannels { imgDict } {
        # Returns key `NumChan` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImageHeight GetImagePixelType GetImagePixelSize GetImageNumPixels GetImageScanOrder GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header NumChan]
    }

    proc GetImageNumPixels { imgDict } {
        # Returns key `NumPixel` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImageHeight GetImagePixelType GetImagePixelSize GetImageNumChannels GetImageScanOrder GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header NumPixel]
    }

    proc GetImageScanOrder { imgDict } {
        # Returns key `ScanOrder` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImageHeight GetImagePixelType GetImagePixelSize GetImageNumChannels GetImageNumPixels GetImageByteOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header ScanOrder]
    }

    proc GetImageByteOrder { imgDict } {
        # Returns key `ByteOrder` from the header of the image dictionary.
        #
        # imgDict - Image dictionary.
        #
        # See [ReadImageHeader] for a description of the header dictionary keys.
        #
        # See also: GetImageWidth GetImageHeight GetImagePixelType GetImagePixelSize GetImageNumChannels GetImageNumPixels GetImageScanOrder

        upvar 1 $imgDict myDict

        return [dict get $myDict Header ByteOrder]
    }
}

pawt::_Init
