# Source code for PIL.ImImagePlugin

    #
    # The Python Imaging Library.
    # $Id$
    #
    # IFUNC IM file handling for PIL
    #
    # history:
    # 1995-09-01 fl   Created.
    # 1997-01-03 fl   Save palette images
    # 1997-01-08 fl   Added sequence support
    # 1997-01-23 fl   Added P and RGB save support
    # 1997-05-31 fl   Read floating point images
    # 1997-06-22 fl   Save floating point images
    # 1997-08-27 fl   Read and save 1-bit images
    # 1998-06-25 fl   Added support for RGB+LUT images
    # 1998-07-02 fl   Added support for YCC images
    # 1998-07-15 fl   Renamed offset attribute to avoid name clash
    # 1998-12-29 fl   Added I;16 support
    # 2001-02-17 fl   Use 're' instead of 'regex' (Python 2.1) (0.7)
    # 2003-09-26 fl   Added LA/PA support
    #
    # Copyright (c) 1997-2003 by Secret Labs AB.
    # Copyright (c) 1995-2001 by Fredrik Lundh.
    #
    # See the README file for information on usage and redistribution.
    #

    import os
    import re

    from . import Image, ImageFile, ImagePalette

    # --------------------------------------------------------------------
    # Standard tags

    COMMENT = "Comment"
    DATE = "Date"
    EQUIPMENT = "Digitalization equipment"
    FRAMES = "File size (no of images)"
    LUT = "Lut"
    NAME = "Name"
    SCALE = "Scale (x,y)"
    SIZE = "Image size (x*y)"
    MODE = "Image type"

    TAGS = {
        COMMENT: 0,
        DATE: 0,
        EQUIPMENT: 0,
        FRAMES: 0,
        LUT: 0,
        NAME: 0,
        SCALE: 0,
        SIZE: 0,
        MODE: 0,
    }

    OPEN = {
        # ifunc93/p3cfunc formats
        "0 1 image": ("1", "1"),
        "L 1 image": ("1", "1"),
        "Greyscale image": ("L", "L"),
        "Grayscale image": ("L", "L"),
        "RGB image": ("RGB", "RGB;L"),
        "RLB image": ("RGB", "RLB"),
        "RYB image": ("RGB", "RLB"),
        "B1 image": ("1", "1"),
        "B2 image": ("P", "P;2"),
        "B4 image": ("P", "P;4"),
        "X 24 image": ("RGB", "RGB"),
        "L 32 S image": ("I", "I;32"),
        "L 32 F image": ("F", "F;32"),
        # old p3cfunc formats
        "RGB3 image": ("RGB", "RGB;T"),
        "RYB3 image": ("RGB", "RYB;T"),
        # extensions
        "LA image": ("LA", "LA;L"),
        "PA image": ("LA", "PA;L"),
        "RGBA image": ("RGBA", "RGBA;L"),
        "RGBX image": ("RGBX", "RGBX;L"),
        "CMYK image": ("CMYK", "CMYK;L"),
        "YCC image": ("YCbCr", "YCbCr;L"),
    }

    # ifunc95 extensions
    for i in ["8", "8S", "16", "16S", "32", "32F"]:
        OPEN[f"L {i} image"] = ("F", f"F;{i}")
        OPEN[f"L*{i} image"] = ("F", f"F;{i}")
    for i in ["16", "16L", "16B"]:
        OPEN[f"L {i} image"] = (f"I;{i}", f"I;{i}")
        OPEN[f"L*{i} image"] = (f"I;{i}", f"I;{i}")
    for i in ["32S"]:
        OPEN[f"L {i} image"] = ("I", f"I;{i}")
        OPEN[f"L*{i} image"] = ("I", f"I;{i}")
    for i in range(2, 33):
        OPEN[f"L*{i} image"] = ("F", f"F;{i}")

    # --------------------------------------------------------------------
    # Read IM directory

    split = re.compile(br"^([A-Za-z][^:]*):[ \t]*(.*)[ \t]*$")

    def number(s):
        try:
            return int(s)
        except ValueError:
            return float(s)

    ##
    # Image plugin for the IFUNC IM file format.

    class ImImageFile(ImageFile.ImageFile):

        format = "IM"
        format_description = "IFUNC Image Memory"
        _close_exclusive_fp_after_loading = False

        def _open(self):

            # Quick rejection: if there's not an LF among the first
            # 100 bytes, this is (probably) not a text header.

            if b"\n" not in self.fp.read(100):
                raise SyntaxError("not an IM file")
            self.fp.seek(0)

            n = 0

            # Default values
            self.info[MODE] = "L"
            self.info[SIZE] = (512, 512)
            self.info[FRAMES] = 1

            self.rawmode = "L"

            while True:

                s = self.fp.read(1)

                # Some versions of IFUNC uses \n\r instead of \r\n...
                if s == b"\r":
                    continue

                if not s or s == b"\0" or s == b"\x1A":
                    break

                # FIXME: this may read whole file if not a text file
                s = s + self.fp.readline()

                if len(s) > 100:
                    raise SyntaxError("not an IM file")

                if s[-2:] == b"\r\n":
                    s = s[:-2]
                elif s[-1:] == b"\n":
                    s = s[:-1]

                try:
                    m = split.match(s)
                except re.error as e:
                    raise SyntaxError("not an IM file") from e

                if m:

                    k, v = m.group(1, 2)

                    # Don't know if this is the correct encoding,
                    # but a decent guess (I guess)
                    k = k.decode("latin-1", "replace")
                    v = v.decode("latin-1", "replace")

                    # Convert value as appropriate
                    if k in [FRAMES, SCALE, SIZE]:
                        v = v.replace("*", ",")
                        v = tuple(map(number, v.split(",")))
                        if len(v) == 1:
                            v = v[0]
                    elif k == MODE and v in OPEN:
                        v, self.rawmode = OPEN[v]

                    # Add to dictionary. Note that COMMENT tags are
                    # combined into a list of strings.
                    if k == COMMENT:
                        if k in self.info:
                            self.info[k].append(v)
                        else:
                            self.info[k] = [v]
                    else:
                        self.info[k] = v

                    if k in TAGS:
                        n += 1

                else:

                    raise SyntaxError(
                        "Syntax error in IM header: " + s.decode("ascii", "replace")
                    )

            if not n:
                raise SyntaxError("Not an IM file")

            # Basic attributes
            self._size = self.info[SIZE]
            self.mode = self.info[MODE]

            # Skip forward to start of image data
            while s and s[0:1] != b"\x1A":
                s = self.fp.read(1)
            if not s:
                raise SyntaxError("File truncated")

            if LUT in self.info:
                # convert lookup table to palette or lut attribute
                palette = self.fp.read(768)
                greyscale = 1  # greyscale palette
                linear = 1  # linear greyscale palette
                for i in range(256):
                    if palette[i] == palette[i + 256] == palette[i + 512]:
                        if palette[i] != i:
                            linear = 0
                    else:
                        greyscale = 0
                if self.mode in ["L", "LA", "P", "PA"]:
                    if greyscale:
                        if not linear:
                            self.lut = list(palette[:256])
                    else:
                        if self.mode in ["L", "P"]:
                            self.mode = self.rawmode = "P"
                        elif self.mode in ["LA", "PA"]:
                            self.mode = "PA"
                            self.rawmode = "PA;L"
                        self.palette = ImagePalette.raw("RGB;L", palette)
                elif self.mode == "RGB":
                    if not greyscale or not linear:
                        self.lut = list(palette)

            self.frame = 0

            self.__offset = offs = self.fp.tell()

            self.__fp = self.fp  # FIXME: hack

            if self.rawmode[:2] == "F;":

                # ifunc95 formats
                try:
                    # use bit decoder (if necessary)
                    bits = int(self.rawmode[2:])
                    if bits not in [8, 16, 32]:
                        self.tile = [("bit", (0, 0) + self.size, offs, (bits, 8, 3, 0, -1))]
                        return
                except ValueError:
                    pass

            if self.rawmode in ["RGB;T", "RYB;T"]:
                # Old LabEye/3PC files.  Would be very surprised if anyone
                # ever stumbled upon such a file ;-)
                size = self.size[0] * self.size[1]
                self.tile = [
                    ("raw", (0, 0) + self.size, offs, ("G", 0, -1)),
                    ("raw", (0, 0) + self.size, offs + size, ("R", 0, -1)),
                    ("raw", (0, 0) + self.size, offs + 2 * size, ("B", 0, -1)),
                ]
            else:
                # LabEye/IFUNC files
                self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]

        @property
        def n_frames(self):
            return self.info[FRAMES]

        @property
        def is_animated(self):
            return self.info[FRAMES] > 1

        def seek(self, frame):
            if not self._seek_check(frame):
                return

            self.frame = frame

            if self.mode == "1":
                bits = 1
            else:
                bits = 8 * len(self.mode)

            size = ((self.size[0] * bits + 7) // 8) * self.size[1]
            offs = self.__offset + frame * size

            self.fp = self.__fp

            self.tile = [("raw", (0, 0) + self.size, offs, (self.rawmode, 0, -1))]

        def tell(self):
            return self.frame

        def _close__fp(self):
            try:
                if self.__fp != self.fp:
                    self.__fp.close()
            except AttributeError:
                pass
            finally:
                self.__fp = None

    #
    # --------------------------------------------------------------------
    # Save IM files

    SAVE = {
        # mode: (im type, raw mode)
        "1": ("0 1", "1"),
        "L": ("Greyscale", "L"),
        "LA": ("LA", "LA;L"),
        "P": ("Greyscale", "P"),
        "PA": ("LA", "PA;L"),
        "I": ("L 32S", "I;32S"),
        "I;16": ("L 16", "I;16"),
        "I;16L": ("L 16L", "I;16L"),
        "I;16B": ("L 16B", "I;16B"),
        "F": ("L 32F", "F;32F"),
        "RGB": ("RGB", "RGB;L"),
        "RGBA": ("RGBA", "RGBA;L"),
        "RGBX": ("RGBX", "RGBX;L"),
        "CMYK": ("CMYK", "CMYK;L"),
        "YCbCr": ("YCC", "YCbCr;L"),
    }

    def _save(im, fp, filename):

        try:
            image_type, rawmode = SAVE[im.mode]
        except KeyError as e:
            raise ValueError(f"Cannot save {im.mode} images as IM") from e

        frames = im.encoderinfo.get("frames", 1)

        fp.write(f"Image type: {image_type} image\r\n".encode("ascii"))
        if filename:
            # Each line must be 100 characters or less,
            # or: SyntaxError("not an IM file")
            # 8 characters are used for "Name: " and "\r\n"
            # Keep just the filename, ditch the potentially overlong path
            name, ext = os.path.splitext(os.path.basename(filename))
            name = "".join([name[: 92 - len(ext)], ext])

            fp.write(f"Name: {name}\r\n".encode("ascii"))
        fp.write(("Image size (x*y): %d*%d\r\n" % im.size).encode("ascii"))
        fp.write(f"File size (no of images): {frames}\r\n".encode("ascii"))
        if im.mode in ["P", "PA"]:
            fp.write(b"Lut: 1\r\n")
        fp.write(b"\000" * (511 - fp.tell()) + b"\032")
        if im.mode in ["P", "PA"]:
            fp.write(im.im.getpalette("RGB", "RGB;L"))  # 768 bytes
        ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, -1))])

    #
    # --------------------------------------------------------------------
    # Registry

    Image.register_open(ImImageFile.format, ImImageFile)
    Image.register_save(ImImageFile.format, _save)

    Image.register_extension(ImImageFile.format, ".im")