{
    "file_diffs": [
        {
            "old_file_content": "import io\nimport struct\n\nimport pytest\n\nfrom PIL import Image, TiffImagePlugin, TiffTags\nfrom PIL.TiffImagePlugin import IFDRational\n\nfrom .helper import assert_deep_equal, hopper\n\nTAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}\n\n\ndef test_rt_metadata(tmp_path):\n    \"\"\"Test writing arbitrary metadata into the tiff image directory\n    Use case is ImageJ private tags, one numeric, one arbitrary\n    data.  https://github.com/python-pillow/Pillow/issues/291\n    \"\"\"\n\n    img = hopper()\n\n    # Behaviour change: re #1416\n    # Pre ifd rewrite, ImageJMetaData was being written as a string(2),\n    # Post ifd rewrite, it's defined as arbitrary bytes(7). It should\n    # roundtrip with the actual bytes, rather than stripped text\n    # of the premerge tests.\n    #\n    # For text items, we still have to decode('ascii','replace') because\n    # the tiff file format can't take 8 bit bytes in that field.\n\n    base_text_data = \"This is some arbitrary metadata for a text field\"\n    bin_data = base_text_data.encode(\"ascii\") + b\" \\xff\"\n    text_data = base_text_data + \" \" + chr(255)\n    reloaded_text_data = base_text_data + \" ?\"\n    float_data = 12.345\n    double_data = 67.89\n    info = TiffImagePlugin.ImageFileDirectory()\n\n    ImageJMetaData = TAG_IDS[\"ImageJMetaData\"]\n    ImageJMetaDataByteCounts = TAG_IDS[\"ImageJMetaDataByteCounts\"]\n    ImageDescription = TAG_IDS[\"ImageDescription\"]\n\n    info[ImageJMetaDataByteCounts] = len(bin_data)\n    info[ImageJMetaData] = bin_data\n    info[TAG_IDS[\"RollAngle\"]] = float_data\n    info.tagtype[TAG_IDS[\"RollAngle\"]] = 11\n    info[TAG_IDS[\"YawAngle\"]] = double_data\n    info.tagtype[TAG_IDS[\"YawAngle\"]] = 12\n\n    info[ImageDescription] = text_data\n\n    f = str(tmp_path / \"temp.tif\")\n\n    img.save(f, tiffinfo=info)\n\n    with Image.open(f) as loaded:\n\n        assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)\n        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)\n\n        assert loaded.tag[ImageJMetaData] == bin_data\n        assert loaded.tag_v2[ImageJMetaData] == bin_data\n\n        assert loaded.tag[ImageDescription] == (reloaded_text_data,)\n        assert loaded.tag_v2[ImageDescription] == reloaded_text_data\n\n        loaded_float = loaded.tag[TAG_IDS[\"RollAngle\"]][0]\n        assert round(abs(loaded_float - float_data), 5) == 0\n        loaded_double = loaded.tag[TAG_IDS[\"YawAngle\"]][0]\n        assert round(abs(loaded_double - double_data), 7) == 0\n\n    # check with 2 element ImageJMetaDataByteCounts, issue #2006\n\n    info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)\n    img.save(f, tiffinfo=info)\n    with Image.open(f) as loaded:\n\n        assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)\n        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)\n\n\ndef test_read_metadata():\n    with Image.open(\"Tests/images/hopper_g4.tif\") as img:\n\n        assert {\n            \"YResolution\": IFDRational(4294967295, 113653537),\n            \"PlanarConfiguration\": 1,\n            \"BitsPerSample\": (1,),\n            \"ImageLength\": 128,\n            \"Compression\": 4,\n            \"FillOrder\": 1,\n            \"RowsPerStrip\": 128,\n            \"ResolutionUnit\": 3,\n            \"PhotometricInterpretation\": 0,\n            \"PageNumber\": (0, 1),\n            \"XResolution\": IFDRational(4294967295, 113653537),\n            \"ImageWidth\": 128,\n            \"Orientation\": 1,\n            \"StripByteCounts\": (1968,),\n            \"SamplesPerPixel\": 1,\n            \"StripOffsets\": (8,),\n        } == img.tag_v2.named()\n\n        assert {\n            \"YResolution\": ((4294967295, 113653537),),\n            \"PlanarConfiguration\": (1,),\n            \"BitsPerSample\": (1,),\n            \"ImageLength\": (128,),\n            \"Compression\": (4,),\n            \"FillOrder\": (1,),\n            \"RowsPerStrip\": (128,),\n            \"ResolutionUnit\": (3,),\n            \"PhotometricInterpretation\": (0,),\n            \"PageNumber\": (0, 1),\n            \"XResolution\": ((4294967295, 113653537),),\n            \"ImageWidth\": (128,),\n            \"Orientation\": (1,),\n            \"StripByteCounts\": (1968,),\n            \"SamplesPerPixel\": (1,),\n            \"StripOffsets\": (8,),\n        } == img.tag.named()\n\n\ndef test_write_metadata(tmp_path):\n    \"\"\"Test metadata writing through the python code\"\"\"\n    with Image.open(\"Tests/images/hopper.tif\") as img:\n        f = str(tmp_path / \"temp.tiff\")\n        img.save(f, tiffinfo=img.tag)\n\n        original = img.tag_v2.named()\n\n    with Image.open(f) as loaded:\n        reloaded = loaded.tag_v2.named()\n\n    ignored = [\"StripByteCounts\", \"RowsPerStrip\", \"PageNumber\", \"StripOffsets\"]\n\n    for tag, value in reloaded.items():\n        if tag in ignored:\n            continue\n        if isinstance(original[tag], tuple) and isinstance(\n            original[tag][0], IFDRational\n        ):\n            # Need to compare element by element in the tuple,\n            # not comparing tuples of object references\n            assert_deep_equal(\n                original[tag],\n                value,\n                f\"{tag} didn't roundtrip, {original[tag]}, {value}\",\n            )\n        else:\n            assert (\n                original[tag] == value\n            ), f\"{tag} didn't roundtrip, {original[tag]}, {value}\"\n\n    for tag, value in original.items():\n        if tag not in ignored:\n            assert value == reloaded[tag], f\"{tag} didn't roundtrip\"\n\n\ndef test_change_stripbytecounts_tag_type(tmp_path):\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.tif\") as im:\n        info = im.tag_v2\n\n        # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT\n        im = im.resize((500, 500))\n\n        # STRIPBYTECOUNTS can be a SHORT or a LONG\n        info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT\n\n        im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG\n\n\ndef test_no_duplicate_50741_tag():\n    assert TAG_IDS[\"MakerNoteSafety\"] == 50741\n    assert TAG_IDS[\"BestQualityScale\"] == 50780\n\n\ndef test_iptc(tmp_path):\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.Lab.tif\") as im:\n        im.save(out)\n\n\ndef test_writing_bytes_to_ascii(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[271]\n    assert tag.type == TiffTags.ASCII\n\n    info[271] = b\"test\"\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[271] == \"test\"\n\n\ndef test_writing_int_to_bytes(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[700]\n    assert tag.type == TiffTags.BYTE\n\n    info[700] = 1\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[700] == b\"\\x01\"\n\n\ndef test_undefined_zero(tmp_path):\n    # Check that the tag has not been changed since this test was created\n    tag = TiffTags.TAGS_V2[45059]\n    assert tag.type == TiffTags.UNDEFINED\n    assert tag.length == 0\n\n    info = TiffImagePlugin.ImageFileDirectory(b\"II*\\x00\\x08\\x00\\x00\\x00\")\n    info[45059] = b\"test\"\n\n    # Assert that the tag value does not change by setting it to itself\n    original = info[45059]\n    info[45059] = info[45059]\n    assert info[45059] == original\n\n\ndef test_empty_metadata():\n    f = io.BytesIO(b\"II*\\x00\\x08\\x00\\x00\\x00\")\n    head = f.read(8)\n    info = TiffImagePlugin.ImageFileDirectory(head)\n    # Should not raise struct.error.\n    pytest.warns(UserWarning, info.load, f)\n\n\ndef test_iccprofile(tmp_path):\n    # https://github.com/python-pillow/Pillow/issues/1462\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.iccprofile.tif\") as im:\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert not isinstance(im.info[\"icc_profile\"], tuple)\n        assert im.info[\"icc_profile\"] == reloaded.info[\"icc_profile\"]\n\n\ndef test_iccprofile_binary():\n    # https://github.com/python-pillow/Pillow/issues/1526\n    # We should be able to load this,\n    # but probably won't be able to save it.\n\n    with Image.open(\"Tests/images/hopper.iccprofile_binary.tif\") as im:\n        assert im.tag_v2.tagtype[34675] == 1\n        assert im.info[\"icc_profile\"]\n\n\ndef test_iccprofile_save_png(tmp_path):\n    with Image.open(\"Tests/images/hopper.iccprofile.tif\") as im:\n        outfile = str(tmp_path / \"temp.png\")\n        im.save(outfile)\n\n\ndef test_iccprofile_binary_save_png(tmp_path):\n    with Image.open(\"Tests/images/hopper.iccprofile_binary.tif\") as im:\n        outfile = str(tmp_path / \"temp.png\")\n        im.save(outfile)\n\n\ndef test_exif_div_zero(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n    info[41988] = TiffImagePlugin.IFDRational(0, 0)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert 0 == reloaded.tag_v2[41988].numerator\n        assert 0 == reloaded.tag_v2[41988].denominator\n\n\ndef test_ifd_unsigned_rational(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    max_long = 2**32 - 1\n\n    # 4 bytes unsigned long\n    numerator = max_long\n\n    info[41493] = TiffImagePlugin.IFDRational(numerator, 1)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert max_long == reloaded.tag_v2[41493].numerator\n        assert 1 == reloaded.tag_v2[41493].denominator\n\n    # out of bounds of 4 byte unsigned long\n    numerator = max_long + 1\n\n    info[41493] = TiffImagePlugin.IFDRational(numerator, 1)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert max_long == reloaded.tag_v2[41493].numerator\n        assert 1 == reloaded.tag_v2[41493].denominator\n\n\ndef test_ifd_signed_rational(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    # pair of 4 byte signed longs\n    numerator = 2**31 - 1\n    denominator = -(2**31)\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert numerator == reloaded.tag_v2[37380].numerator\n        assert denominator == reloaded.tag_v2[37380].denominator\n\n    numerator = -(2**31)\n    denominator = 2**31 - 1\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert numerator == reloaded.tag_v2[37380].numerator\n        assert denominator == reloaded.tag_v2[37380].denominator\n\n    # out of bounds of 4 byte signed long\n    numerator = -(2**31) - 1\n    denominator = 1\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert 2**31 - 1 == reloaded.tag_v2[37380].numerator\n        assert -1 == reloaded.tag_v2[37380].denominator\n\n\ndef test_ifd_signed_long(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    info[37000] = -60000\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[37000] == -60000\n\n\ndef test_empty_values():\n    data = io.BytesIO(\n        b\"II*\\x00\\x08\\x00\\x00\\x00\\x03\\x00\\x1a\\x01\\x05\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x1b\\x01\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x98\\x82\\x02\\x00\\x07\\x00\\x00\\x002\\x00\\x00\\x00\\x00\\x00\\x00\\x00a \"\n        b\"text\\x00\\x00\"\n    )\n    head = data.read(8)\n    info = TiffImagePlugin.ImageFileDirectory_v2(head)\n    info.load(data)\n    # Should not raise ValueError.\n    info = dict(info)\n    assert 33432 in info\n\n\ndef test_photoshop_info(tmp_path):\n    with Image.open(\"Tests/images/issue_2278.tif\") as im:\n        assert len(im.tag_v2[34377]) == 70\n        assert isinstance(im.tag_v2[34377], bytes)\n        out = str(tmp_path / \"temp.tiff\")\n        im.save(out)\n    with Image.open(out) as reloaded:\n        assert len(reloaded.tag_v2[34377]) == 70\n        assert isinstance(reloaded.tag_v2[34377], bytes)\n\n\ndef test_too_many_entries():\n    ifd = TiffImagePlugin.ImageFileDirectory_v2()\n\n    #    277: (\"SamplesPerPixel\", SHORT, 1),\n    ifd._tagdata[277] = struct.pack(\"hh\", 4, 4)\n    ifd.tagtype[277] = TiffTags.SHORT\n\n    # Should not raise ValueError.\n    pytest.warns(UserWarning, lambda: ifd[277])\n\n\ndef test_tag_group_data():\n    base_ifd = TiffImagePlugin.ImageFileDirectory_v2()\n    interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)\n    for ifd in (base_ifd, interop_ifd):\n        ifd[2] = \"test\"\n        ifd[256] = 10\n\n    assert base_ifd.tagtype[256] == 4\n    assert interop_ifd.tagtype[256] != base_ifd.tagtype[256]\n\n    assert interop_ifd.tagtype[2] == 7\n    assert base_ifd.tagtype[2] != interop_ifd.tagtype[256]\n\n\ndef test_empty_subifd(tmp_path):\n    out = str(tmp_path / \"temp.jpg\")\n\n    im = hopper()\n    exif = im.getexif()\n    exif[TiffImagePlugin.EXIFIFD] = {}\n    im.save(out, exif=exif)\n\n    with Image.open(out) as reloaded:\n        exif = reloaded.getexif()\n        assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {}\n",
            "new_file_content": "import io\nimport struct\n\nimport pytest\n\nfrom PIL import Image, TiffImagePlugin, TiffTags\nfrom PIL.TiffImagePlugin import IFDRational\n\nfrom .helper import assert_deep_equal, hopper\n\nTAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}\n\n\ndef test_rt_metadata(tmp_path):\n    \"\"\"Test writing arbitrary metadata into the tiff image directory\n    Use case is ImageJ private tags, one numeric, one arbitrary\n    data.  https://github.com/python-pillow/Pillow/issues/291\n    \"\"\"\n\n    img = hopper()\n\n    # Behaviour change: re #1416\n    # Pre ifd rewrite, ImageJMetaData was being written as a string(2),\n    # Post ifd rewrite, it's defined as arbitrary bytes(7). It should\n    # roundtrip with the actual bytes, rather than stripped text\n    # of the premerge tests.\n    #\n    # For text items, we still have to decode('ascii','replace') because\n    # the tiff file format can't take 8 bit bytes in that field.\n\n    base_text_data = \"This is some arbitrary metadata for a text field\"\n    bin_data = base_text_data.encode(\"ascii\") + b\" \\xff\"\n    text_data = base_text_data + \" \" + chr(255)\n    reloaded_text_data = base_text_data + \" ?\"\n    float_data = 12.345\n    double_data = 67.89\n    info = TiffImagePlugin.ImageFileDirectory()\n\n    ImageJMetaData = TAG_IDS[\"ImageJMetaData\"]\n    ImageJMetaDataByteCounts = TAG_IDS[\"ImageJMetaDataByteCounts\"]\n    ImageDescription = TAG_IDS[\"ImageDescription\"]\n\n    info[ImageJMetaDataByteCounts] = len(bin_data)\n    info[ImageJMetaData] = bin_data\n    info[TAG_IDS[\"RollAngle\"]] = float_data\n    info.tagtype[TAG_IDS[\"RollAngle\"]] = 11\n    info[TAG_IDS[\"YawAngle\"]] = double_data\n    info.tagtype[TAG_IDS[\"YawAngle\"]] = 12\n\n    info[ImageDescription] = text_data\n\n    f = str(tmp_path / \"temp.tif\")\n\n    img.save(f, tiffinfo=info)\n\n    with Image.open(f) as loaded:\n\n        assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)\n        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)\n\n        assert loaded.tag[ImageJMetaData] == bin_data\n        assert loaded.tag_v2[ImageJMetaData] == bin_data\n\n        assert loaded.tag[ImageDescription] == (reloaded_text_data,)\n        assert loaded.tag_v2[ImageDescription] == reloaded_text_data\n\n        loaded_float = loaded.tag[TAG_IDS[\"RollAngle\"]][0]\n        assert round(abs(loaded_float - float_data), 5) == 0\n        loaded_double = loaded.tag[TAG_IDS[\"YawAngle\"]][0]\n        assert round(abs(loaded_double - double_data), 7) == 0\n\n    # check with 2 element ImageJMetaDataByteCounts, issue #2006\n\n    info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)\n    img.save(f, tiffinfo=info)\n    with Image.open(f) as loaded:\n\n        assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)\n        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)\n\n\ndef test_read_metadata():\n    with Image.open(\"Tests/images/hopper_g4.tif\") as img:\n\n        assert {\n            \"YResolution\": IFDRational(4294967295, 113653537),\n            \"PlanarConfiguration\": 1,\n            \"BitsPerSample\": (1,),\n            \"ImageLength\": 128,\n            \"Compression\": 4,\n            \"FillOrder\": 1,\n            \"RowsPerStrip\": 128,\n            \"ResolutionUnit\": 3,\n            \"PhotometricInterpretation\": 0,\n            \"PageNumber\": (0, 1),\n            \"XResolution\": IFDRational(4294967295, 113653537),\n            \"ImageWidth\": 128,\n            \"Orientation\": 1,\n            \"StripByteCounts\": (1968,),\n            \"SamplesPerPixel\": 1,\n            \"StripOffsets\": (8,),\n        } == img.tag_v2.named()\n\n        assert {\n            \"YResolution\": ((4294967295, 113653537),),\n            \"PlanarConfiguration\": (1,),\n            \"BitsPerSample\": (1,),\n            \"ImageLength\": (128,),\n            \"Compression\": (4,),\n            \"FillOrder\": (1,),\n            \"RowsPerStrip\": (128,),\n            \"ResolutionUnit\": (3,),\n            \"PhotometricInterpretation\": (0,),\n            \"PageNumber\": (0, 1),\n            \"XResolution\": ((4294967295, 113653537),),\n            \"ImageWidth\": (128,),\n            \"Orientation\": (1,),\n            \"StripByteCounts\": (1968,),\n            \"SamplesPerPixel\": (1,),\n            \"StripOffsets\": (8,),\n        } == img.tag.named()\n\n\ndef test_write_metadata(tmp_path):\n    \"\"\"Test metadata writing through the python code\"\"\"\n    with Image.open(\"Tests/images/hopper.tif\") as img:\n        f = str(tmp_path / \"temp.tiff\")\n        img.save(f, tiffinfo=img.tag)\n\n        original = img.tag_v2.named()\n\n    with Image.open(f) as loaded:\n        reloaded = loaded.tag_v2.named()\n\n    ignored = [\"StripByteCounts\", \"RowsPerStrip\", \"PageNumber\", \"StripOffsets\"]\n\n    for tag, value in reloaded.items():\n        if tag in ignored:\n            continue\n        if isinstance(original[tag], tuple) and isinstance(\n            original[tag][0], IFDRational\n        ):\n            # Need to compare element by element in the tuple,\n            # not comparing tuples of object references\n            assert_deep_equal(\n                original[tag],\n                value,\n                f\"{tag} didn't roundtrip, {original[tag]}, {value}\",\n            )\n        else:\n            assert (\n                original[tag] == value\n            ), f\"{tag} didn't roundtrip, {original[tag]}, {value}\"\n\n    for tag, value in original.items():\n        if tag not in ignored:\n            assert value == reloaded[tag], f\"{tag} didn't roundtrip\"\n\n\ndef test_change_stripbytecounts_tag_type(tmp_path):\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.tif\") as im:\n        info = im.tag_v2\n\n        # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT\n        im = im.resize((500, 500))\n\n        # STRIPBYTECOUNTS can be a SHORT or a LONG\n        info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT\n\n        im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG\n\n\ndef test_no_duplicate_50741_tag():\n    assert TAG_IDS[\"MakerNoteSafety\"] == 50741\n    assert TAG_IDS[\"BestQualityScale\"] == 50780\n\n\ndef test_iptc(tmp_path):\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.Lab.tif\") as im:\n        im.save(out)\n\n\ndef test_writing_other_types_to_ascii(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[271]\n    assert tag.type == TiffTags.ASCII\n\n    out = str(tmp_path / \"temp.tiff\")\n    for (value, expected) in {b\"test\": \"test\", 1: \"1\"}.items():\n        info[271] = value\n\n        im.save(out, tiffinfo=info)\n\n        with Image.open(out) as reloaded:\n            assert reloaded.tag_v2[271] == expected\n\n\ndef test_writing_int_to_bytes(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[700]\n    assert tag.type == TiffTags.BYTE\n\n    info[700] = 1\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[700] == b\"\\x01\"\n\n\ndef test_undefined_zero(tmp_path):\n    # Check that the tag has not been changed since this test was created\n    tag = TiffTags.TAGS_V2[45059]\n    assert tag.type == TiffTags.UNDEFINED\n    assert tag.length == 0\n\n    info = TiffImagePlugin.ImageFileDirectory(b\"II*\\x00\\x08\\x00\\x00\\x00\")\n    info[45059] = b\"test\"\n\n    # Assert that the tag value does not change by setting it to itself\n    original = info[45059]\n    info[45059] = info[45059]\n    assert info[45059] == original\n\n\ndef test_empty_metadata():\n    f = io.BytesIO(b\"II*\\x00\\x08\\x00\\x00\\x00\")\n    head = f.read(8)\n    info = TiffImagePlugin.ImageFileDirectory(head)\n    # Should not raise struct.error.\n    pytest.warns(UserWarning, info.load, f)\n\n\ndef test_iccprofile(tmp_path):\n    # https://github.com/python-pillow/Pillow/issues/1462\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.iccprofile.tif\") as im:\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert not isinstance(im.info[\"icc_profile\"], tuple)\n        assert im.info[\"icc_profile\"] == reloaded.info[\"icc_profile\"]\n\n\ndef test_iccprofile_binary():\n    # https://github.com/python-pillow/Pillow/issues/1526\n    # We should be able to load this,\n    # but probably won't be able to save it.\n\n    with Image.open(\"Tests/images/hopper.iccprofile_binary.tif\") as im:\n        assert im.tag_v2.tagtype[34675] == 1\n        assert im.info[\"icc_profile\"]\n\n\ndef test_iccprofile_save_png(tmp_path):\n    with Image.open(\"Tests/images/hopper.iccprofile.tif\") as im:\n        outfile = str(tmp_path / \"temp.png\")\n        im.save(outfile)\n\n\ndef test_iccprofile_binary_save_png(tmp_path):\n    with Image.open(\"Tests/images/hopper.iccprofile_binary.tif\") as im:\n        outfile = str(tmp_path / \"temp.png\")\n        im.save(outfile)\n\n\ndef test_exif_div_zero(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n    info[41988] = TiffImagePlugin.IFDRational(0, 0)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert 0 == reloaded.tag_v2[41988].numerator\n        assert 0 == reloaded.tag_v2[41988].denominator\n\n\ndef test_ifd_unsigned_rational(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    max_long = 2**32 - 1\n\n    # 4 bytes unsigned long\n    numerator = max_long\n\n    info[41493] = TiffImagePlugin.IFDRational(numerator, 1)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert max_long == reloaded.tag_v2[41493].numerator\n        assert 1 == reloaded.tag_v2[41493].denominator\n\n    # out of bounds of 4 byte unsigned long\n    numerator = max_long + 1\n\n    info[41493] = TiffImagePlugin.IFDRational(numerator, 1)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert max_long == reloaded.tag_v2[41493].numerator\n        assert 1 == reloaded.tag_v2[41493].denominator\n\n\ndef test_ifd_signed_rational(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    # pair of 4 byte signed longs\n    numerator = 2**31 - 1\n    denominator = -(2**31)\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert numerator == reloaded.tag_v2[37380].numerator\n        assert denominator == reloaded.tag_v2[37380].denominator\n\n    numerator = -(2**31)\n    denominator = 2**31 - 1\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert numerator == reloaded.tag_v2[37380].numerator\n        assert denominator == reloaded.tag_v2[37380].denominator\n\n    # out of bounds of 4 byte signed long\n    numerator = -(2**31) - 1\n    denominator = 1\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert 2**31 - 1 == reloaded.tag_v2[37380].numerator\n        assert -1 == reloaded.tag_v2[37380].denominator\n\n\ndef test_ifd_signed_long(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    info[37000] = -60000\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[37000] == -60000\n\n\ndef test_empty_values():\n    data = io.BytesIO(\n        b\"II*\\x00\\x08\\x00\\x00\\x00\\x03\\x00\\x1a\\x01\\x05\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x1b\\x01\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x98\\x82\\x02\\x00\\x07\\x00\\x00\\x002\\x00\\x00\\x00\\x00\\x00\\x00\\x00a \"\n        b\"text\\x00\\x00\"\n    )\n    head = data.read(8)\n    info = TiffImagePlugin.ImageFileDirectory_v2(head)\n    info.load(data)\n    # Should not raise ValueError.\n    info = dict(info)\n    assert 33432 in info\n\n\ndef test_photoshop_info(tmp_path):\n    with Image.open(\"Tests/images/issue_2278.tif\") as im:\n        assert len(im.tag_v2[34377]) == 70\n        assert isinstance(im.tag_v2[34377], bytes)\n        out = str(tmp_path / \"temp.tiff\")\n        im.save(out)\n    with Image.open(out) as reloaded:\n        assert len(reloaded.tag_v2[34377]) == 70\n        assert isinstance(reloaded.tag_v2[34377], bytes)\n\n\ndef test_too_many_entries():\n    ifd = TiffImagePlugin.ImageFileDirectory_v2()\n\n    #    277: (\"SamplesPerPixel\", SHORT, 1),\n    ifd._tagdata[277] = struct.pack(\"hh\", 4, 4)\n    ifd.tagtype[277] = TiffTags.SHORT\n\n    # Should not raise ValueError.\n    pytest.warns(UserWarning, lambda: ifd[277])\n\n\ndef test_tag_group_data():\n    base_ifd = TiffImagePlugin.ImageFileDirectory_v2()\n    interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)\n    for ifd in (base_ifd, interop_ifd):\n        ifd[2] = \"test\"\n        ifd[256] = 10\n\n    assert base_ifd.tagtype[256] == 4\n    assert interop_ifd.tagtype[256] != base_ifd.tagtype[256]\n\n    assert interop_ifd.tagtype[2] == 7\n    assert base_ifd.tagtype[2] != interop_ifd.tagtype[256]\n\n\ndef test_empty_subifd(tmp_path):\n    out = str(tmp_path / \"temp.jpg\")\n\n    im = hopper()\n    exif = im.getexif()\n    exif[TiffImagePlugin.EXIFIFD] = {}\n    im.save(out, exif=exif)\n\n    with Image.open(out) as reloaded:\n        exif = reloaded.getexif()\n        assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {}\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_tiff_metadata.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "b90dde3d9",
                "new_commit_hash": "48c0273fe",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Tests/test_file_tiff_metadata.py"
            },
            "plus_file": {
                "path": "b/Tests/test_file_tiff_metadata.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 185,
                            "length": 20
                        },
                        "new_range": {
                            "start": 185,
                            "length": 21
                        },
                        "section": "def test_iptc(tmp_path):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "        im.save(out)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_writing_bytes_to_ascii(tmp_path):",
                                "type": "deleted"
                            },
                            {
                                "content": "def test_writing_other_types_to_ascii(tmp_path):",
                                "type": "added"
                            },
                            {
                                "content": "    im = hopper()",
                                "type": "context"
                            },
                            {
                                "content": "    info = TiffImagePlugin.ImageFileDirectory_v2()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    tag = TiffTags.TAGS_V2[271]",
                                "type": "context"
                            },
                            {
                                "content": "    assert tag.type == TiffTags.ASCII",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    info[271] = b\"test\"",
                                "type": "deleted"
                            },
                            {
                                "content": "",
                                "type": "deleted"
                            },
                            {
                                "content": "    out = str(tmp_path / \"temp.tiff\")",
                                "type": "context"
                            },
                            {
                                "content": "    im.save(out, tiffinfo=info)",
                                "type": "deleted"
                            },
                            {
                                "content": "    for (value, expected) in {b\"test\": \"test\", 1: \"1\"}.items():",
                                "type": "added"
                            },
                            {
                                "content": "        info[271] = value",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(out) as reloaded:",
                                "type": "deleted"
                            },
                            {
                                "content": "        assert reloaded.tag_v2[271] == \"test\"",
                                "type": "deleted"
                            },
                            {
                                "content": "        im.save(out, tiffinfo=info)",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "        with Image.open(out) as reloaded:",
                                "type": "added"
                            },
                            {
                                "content": "            assert reloaded.tag_v2[271] == expected",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_writing_int_to_bytes(tmp_path):",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_tiff_metadata.py",
                            "type": "function",
                            "name": "test_writing_other_types_to_ascii",
                            "content": "def test_writing_other_types_to_ascii(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[271]\n    assert tag.type == TiffTags.ASCII\n\n    out = str(tmp_path / \"temp.tiff\")\n    for (value, expected) in {b\"test\": \"test\", 1: \"1\"}.items():\n        info[271] = value\n\n        im.save(out, tiffinfo=info)\n\n        with Image.open(out) as reloaded:\n            assert reloaded.tag_v2[271] == expected",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 188,
                            "end_lineno": 202,
                            "parent": null
                        }
                    ],
                    "deleted_entities": [
                        {
                            "file_name": "Tests/test_file_tiff_metadata.py",
                            "type": "function",
                            "name": "test_writing_bytes_to_ascii",
                            "content": "def test_writing_bytes_to_ascii(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[271]\n    assert tag.type == TiffTags.ASCII\n\n    info[271] = b\"test\"\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[271] == \"test\"",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 188,
                            "end_lineno": 201,
                            "parent": null
                        }
                    ]
                }
            ]
        },
        {
            "old_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# TIFF file handling\n#\n# TIFF is a flexible, if somewhat aged, image file format originally\n# defined by Aldus.  Although TIFF supports a wide variety of pixel\n# layouts and compression methods, the name doesn't really stand for\n# \"thousands of incompatible file formats,\" it just feels that way.\n#\n# To read TIFF data from a stream, the stream must be seekable.  For\n# progressive decoding, make sure to use TIFF files where the tag\n# directory is placed first in the file.\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-05-04 fl   Handle JPEGTABLES tag\n# 1996-05-18 fl   Fixed COLORMAP support\n# 1997-01-05 fl   Fixed PREDICTOR support\n# 1997-08-27 fl   Added support for rational tags (from Perry Stoll)\n# 1998-01-10 fl   Fixed seek/tell (from Jan Blom)\n# 1998-07-15 fl   Use private names for internal variables\n# 1999-06-13 fl   Rewritten for PIL 1.0 (1.0)\n# 2000-10-11 fl   Additional fixes for Python 2.0 (1.1)\n# 2001-04-17 fl   Fixed rewind support (seek to frame 0) (1.2)\n# 2001-05-12 fl   Added write support for more tags (from Greg Couch) (1.3)\n# 2001-12-18 fl   Added workaround for broken Matrox library\n# 2002-01-18 fl   Don't mess up if photometric tag is missing (D. Alan Stewart)\n# 2003-05-19 fl   Check FILLORDER tag\n# 2003-09-26 fl   Added RGBa support\n# 2004-02-24 fl   Added DPI support; fixed rational write support\n# 2005-02-07 fl   Added workaround for broken Corel Draw 10 files\n# 2006-01-09 fl   Added support for float/double tags (from Russell Nelson)\n#\n# Copyright (c) 1997-2006 by Secret Labs AB.  All rights reserved.\n# Copyright (c) 1995-1997 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\nimport io\nimport itertools\nimport logging\nimport math\nimport os\nimport struct\nimport warnings\nfrom collections.abc import MutableMapping\nfrom fractions import Fraction\nfrom numbers import Number, Rational\n\nfrom . import Image, ImageFile, ImageOps, ImagePalette, TiffTags\nfrom ._binary import i16be as i16\nfrom ._binary import i32be as i32\nfrom ._binary import o8\nfrom .TiffTags import TYPES\n\nlogger = logging.getLogger(__name__)\n\n# Set these to true to force use of libtiff for reading or writing.\nREAD_LIBTIFF = False\nWRITE_LIBTIFF = False\nIFD_LEGACY_API = True\nSTRIP_SIZE = 65536\n\nII = b\"II\"  # little-endian (Intel style)\nMM = b\"MM\"  # big-endian (Motorola style)\n\n#\n# --------------------------------------------------------------------\n# Read TIFF files\n\n# a few tag names, just to make the code below a bit more readable\nIMAGEWIDTH = 256\nIMAGELENGTH = 257\nBITSPERSAMPLE = 258\nCOMPRESSION = 259\nPHOTOMETRIC_INTERPRETATION = 262\nFILLORDER = 266\nIMAGEDESCRIPTION = 270\nSTRIPOFFSETS = 273\nSAMPLESPERPIXEL = 277\nROWSPERSTRIP = 278\nSTRIPBYTECOUNTS = 279\nX_RESOLUTION = 282\nY_RESOLUTION = 283\nPLANAR_CONFIGURATION = 284\nRESOLUTION_UNIT = 296\nTRANSFERFUNCTION = 301\nSOFTWARE = 305\nDATE_TIME = 306\nARTIST = 315\nPREDICTOR = 317\nCOLORMAP = 320\nTILEWIDTH = 322\nTILELENGTH = 323\nTILEOFFSETS = 324\nTILEBYTECOUNTS = 325\nSUBIFD = 330\nEXTRASAMPLES = 338\nSAMPLEFORMAT = 339\nJPEGTABLES = 347\nYCBCRSUBSAMPLING = 530\nREFERENCEBLACKWHITE = 532\nCOPYRIGHT = 33432\nIPTC_NAA_CHUNK = 33723  # newsphoto properties\nPHOTOSHOP_CHUNK = 34377  # photoshop properties\nICCPROFILE = 34675\nEXIFIFD = 34665\nXMP = 700\nJPEGQUALITY = 65537  # pseudo-tag by libtiff\n\n# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java\nIMAGEJ_META_DATA_BYTE_COUNTS = 50838\nIMAGEJ_META_DATA = 50839\n\nCOMPRESSION_INFO = {\n    # Compression => pil compression name\n    1: \"raw\",\n    2: \"tiff_ccitt\",\n    3: \"group3\",\n    4: \"group4\",\n    5: \"tiff_lzw\",\n    6: \"tiff_jpeg\",  # obsolete\n    7: \"jpeg\",\n    8: \"tiff_adobe_deflate\",\n    32771: \"tiff_raw_16\",  # 16-bit padding\n    32773: \"packbits\",\n    32809: \"tiff_thunderscan\",\n    32946: \"tiff_deflate\",\n    34676: \"tiff_sgilog\",\n    34677: \"tiff_sgilog24\",\n    34925: \"lzma\",\n    50000: \"zstd\",\n    50001: \"webp\",\n}\n\nCOMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}\n\nOPEN_INFO = {\n    # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,\n    #  ExtraSamples) => mode, rawmode\n    (II, 0, (1,), 1, (1,), ()): (\"1\", \"1;I\"),\n    (MM, 0, (1,), 1, (1,), ()): (\"1\", \"1;I\"),\n    (II, 0, (1,), 2, (1,), ()): (\"1\", \"1;IR\"),\n    (MM, 0, (1,), 2, (1,), ()): (\"1\", \"1;IR\"),\n    (II, 1, (1,), 1, (1,), ()): (\"1\", \"1\"),\n    (MM, 1, (1,), 1, (1,), ()): (\"1\", \"1\"),\n    (II, 1, (1,), 2, (1,), ()): (\"1\", \"1;R\"),\n    (MM, 1, (1,), 2, (1,), ()): (\"1\", \"1;R\"),\n    (II, 0, (1,), 1, (2,), ()): (\"L\", \"L;2I\"),\n    (MM, 0, (1,), 1, (2,), ()): (\"L\", \"L;2I\"),\n    (II, 0, (1,), 2, (2,), ()): (\"L\", \"L;2IR\"),\n    (MM, 0, (1,), 2, (2,), ()): (\"L\", \"L;2IR\"),\n    (II, 1, (1,), 1, (2,), ()): (\"L\", \"L;2\"),\n    (MM, 1, (1,), 1, (2,), ()): (\"L\", \"L;2\"),\n    (II, 1, (1,), 2, (2,), ()): (\"L\", \"L;2R\"),\n    (MM, 1, (1,), 2, (2,), ()): (\"L\", \"L;2R\"),\n    (II, 0, (1,), 1, (4,), ()): (\"L\", \"L;4I\"),\n    (MM, 0, (1,), 1, (4,), ()): (\"L\", \"L;4I\"),\n    (II, 0, (1,), 2, (4,), ()): (\"L\", \"L;4IR\"),\n    (MM, 0, (1,), 2, (4,), ()): (\"L\", \"L;4IR\"),\n    (II, 1, (1,), 1, (4,), ()): (\"L\", \"L;4\"),\n    (MM, 1, (1,), 1, (4,), ()): (\"L\", \"L;4\"),\n    (II, 1, (1,), 2, (4,), ()): (\"L\", \"L;4R\"),\n    (MM, 1, (1,), 2, (4,), ()): (\"L\", \"L;4R\"),\n    (II, 0, (1,), 1, (8,), ()): (\"L\", \"L;I\"),\n    (MM, 0, (1,), 1, (8,), ()): (\"L\", \"L;I\"),\n    (II, 0, (1,), 2, (8,), ()): (\"L\", \"L;IR\"),\n    (MM, 0, (1,), 2, (8,), ()): (\"L\", \"L;IR\"),\n    (II, 1, (1,), 1, (8,), ()): (\"L\", \"L\"),\n    (MM, 1, (1,), 1, (8,), ()): (\"L\", \"L\"),\n    (II, 1, (1,), 2, (8,), ()): (\"L\", \"L;R\"),\n    (MM, 1, (1,), 2, (8,), ()): (\"L\", \"L;R\"),\n    (II, 1, (1,), 1, (12,), ()): (\"I;16\", \"I;12\"),\n    (II, 0, (1,), 1, (16,), ()): (\"I;16\", \"I;16\"),\n    (II, 1, (1,), 1, (16,), ()): (\"I;16\", \"I;16\"),\n    (MM, 1, (1,), 1, (16,), ()): (\"I;16B\", \"I;16B\"),\n    (II, 1, (1,), 2, (16,), ()): (\"I;16\", \"I;16R\"),\n    (II, 1, (2,), 1, (16,), ()): (\"I\", \"I;16S\"),\n    (MM, 1, (2,), 1, (16,), ()): (\"I\", \"I;16BS\"),\n    (II, 0, (3,), 1, (32,), ()): (\"F\", \"F;32F\"),\n    (MM, 0, (3,), 1, (32,), ()): (\"F\", \"F;32BF\"),\n    (II, 1, (1,), 1, (32,), ()): (\"I\", \"I;32N\"),\n    (II, 1, (2,), 1, (32,), ()): (\"I\", \"I;32S\"),\n    (MM, 1, (2,), 1, (32,), ()): (\"I\", \"I;32BS\"),\n    (II, 1, (3,), 1, (32,), ()): (\"F\", \"F;32F\"),\n    (MM, 1, (3,), 1, (32,), ()): (\"F\", \"F;32BF\"),\n    (II, 1, (1,), 1, (8, 8), (2,)): (\"LA\", \"LA\"),\n    (MM, 1, (1,), 1, (8, 8), (2,)): (\"LA\", \"LA\"),\n    (II, 2, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGB\"),\n    (MM, 2, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGB\"),\n    (II, 2, (1,), 2, (8, 8, 8), ()): (\"RGB\", \"RGB;R\"),\n    (MM, 2, (1,), 2, (8, 8, 8), ()): (\"RGB\", \"RGB;R\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), ()): (\"RGBA\", \"RGBA\"),  # missing ExtraSamples\n    (MM, 2, (1,), 1, (8, 8, 8, 8), ()): (\"RGBA\", \"RGBA\"),  # missing ExtraSamples\n    (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): (\"RGBX\", \"RGBX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): (\"RGBX\", \"RGBX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): (\"RGBX\", \"RGBXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): (\"RGBX\", \"RGBXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): (\"RGBX\", \"RGBXXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): (\"RGBX\", \"RGBXXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): (\"RGBA\", \"RGBa\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): (\"RGBA\", \"RGBa\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): (\"RGBA\", \"RGBaX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): (\"RGBA\", \"RGBaX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): (\"RGBA\", \"RGBaXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): (\"RGBA\", \"RGBaXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): (\"RGBA\", \"RGBA\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): (\"RGBA\", \"RGBA\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): (\"RGBA\", \"RGBAX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): (\"RGBA\", \"RGBAX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): (\"RGBA\", \"RGBAXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): (\"RGBA\", \"RGBAXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): (\"RGBA\", \"RGBA\"),  # Corel Draw 10\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): (\"RGBA\", \"RGBA\"),  # Corel Draw 10\n    (II, 2, (1,), 1, (16, 16, 16), ()): (\"RGB\", \"RGB;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16), ()): (\"RGB\", \"RGB;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), ()): (\"RGBA\", \"RGBA;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), ()): (\"RGBA\", \"RGBA;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): (\"RGBX\", \"RGBX;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): (\"RGBX\", \"RGBX;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): (\"RGBA\", \"RGBa;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): (\"RGBA\", \"RGBa;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): (\"RGBA\", \"RGBA;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): (\"RGBA\", \"RGBA;16B\"),\n    (II, 3, (1,), 1, (1,), ()): (\"P\", \"P;1\"),\n    (MM, 3, (1,), 1, (1,), ()): (\"P\", \"P;1\"),\n    (II, 3, (1,), 2, (1,), ()): (\"P\", \"P;1R\"),\n    (MM, 3, (1,), 2, (1,), ()): (\"P\", \"P;1R\"),\n    (II, 3, (1,), 1, (2,), ()): (\"P\", \"P;2\"),\n    (MM, 3, (1,), 1, (2,), ()): (\"P\", \"P;2\"),\n    (II, 3, (1,), 2, (2,), ()): (\"P\", \"P;2R\"),\n    (MM, 3, (1,), 2, (2,), ()): (\"P\", \"P;2R\"),\n    (II, 3, (1,), 1, (4,), ()): (\"P\", \"P;4\"),\n    (MM, 3, (1,), 1, (4,), ()): (\"P\", \"P;4\"),\n    (II, 3, (1,), 2, (4,), ()): (\"P\", \"P;4R\"),\n    (MM, 3, (1,), 2, (4,), ()): (\"P\", \"P;4R\"),\n    (II, 3, (1,), 1, (8,), ()): (\"P\", \"P\"),\n    (MM, 3, (1,), 1, (8,), ()): (\"P\", \"P\"),\n    (II, 3, (1,), 1, (8, 8), (2,)): (\"PA\", \"PA\"),\n    (MM, 3, (1,), 1, (8, 8), (2,)): (\"PA\", \"PA\"),\n    (II, 3, (1,), 2, (8,), ()): (\"P\", \"P;R\"),\n    (MM, 3, (1,), 2, (8,), ()): (\"P\", \"P;R\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8), ()): (\"CMYK\", \"CMYK\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8), ()): (\"CMYK\", \"CMYK\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): (\"CMYK\", \"CMYKX\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): (\"CMYK\", \"CMYKX\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): (\"CMYK\", \"CMYKXX\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): (\"CMYK\", \"CMYKXX\"),\n    (II, 5, (1,), 1, (16, 16, 16, 16), ()): (\"CMYK\", \"CMYK;16L\"),\n    # JPEG compressed images handled by LibTiff and auto-converted to RGBX\n    # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel\n    (II, 6, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGBX\"),\n    (MM, 6, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGBX\"),\n    (II, 8, (1,), 1, (8, 8, 8), ()): (\"LAB\", \"LAB\"),\n    (MM, 8, (1,), 1, (8, 8, 8), ()): (\"LAB\", \"LAB\"),\n}\n\nMAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())\n\nPREFIXES = [\n    b\"MM\\x00\\x2A\",  # Valid TIFF header with big-endian byte order\n    b\"II\\x2A\\x00\",  # Valid TIFF header with little-endian byte order\n    b\"MM\\x2A\\x00\",  # Invalid TIFF header, assume big-endian\n    b\"II\\x00\\x2A\",  # Invalid TIFF header, assume little-endian\n    b\"MM\\x00\\x2B\",  # BigTIFF with big-endian byte order\n    b\"II\\x2B\\x00\",  # BigTIFF with little-endian byte order\n]\n\n\ndef _accept(prefix):\n    return prefix[:4] in PREFIXES\n\n\ndef _limit_rational(val, max_val):\n    inv = abs(val) > 1\n    n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)\n    return n_d[::-1] if inv else n_d\n\n\ndef _limit_signed_rational(val, max_val, min_val):\n    frac = Fraction(val)\n    n_d = frac.numerator, frac.denominator\n\n    if min(n_d) < min_val:\n        n_d = _limit_rational(val, abs(min_val))\n\n    if max(n_d) > max_val:\n        val = Fraction(*n_d)\n        n_d = _limit_rational(val, max_val)\n\n    return n_d\n\n\n##\n# Wrapper for TIFF IFDs.\n\n_load_dispatch = {}\n_write_dispatch = {}\n\n\nclass IFDRational(Rational):\n    \"\"\"Implements a rational class where 0/0 is a legal value to match\n    the in the wild use of exif rationals.\n\n    e.g., DigitalZoomRatio - 0.00/0.00  indicates that no digital zoom was used\n    \"\"\"\n\n    \"\"\" If the denominator is 0, store this as a float('nan'), otherwise store\n    as a fractions.Fraction(). Delegate as appropriate\n\n    \"\"\"\n\n    __slots__ = (\"_numerator\", \"_denominator\", \"_val\")\n\n    def __init__(self, value, denominator=1):\n        \"\"\"\n        :param value: either an integer numerator, a\n        float/rational/other number, or an IFDRational\n        :param denominator: Optional integer denominator\n        \"\"\"\n        if isinstance(value, IFDRational):\n            self._numerator = value.numerator\n            self._denominator = value.denominator\n            self._val = value._val\n            return\n\n        if isinstance(value, Fraction):\n            self._numerator = value.numerator\n            self._denominator = value.denominator\n        else:\n            self._numerator = value\n            self._denominator = denominator\n\n        if denominator == 0:\n            self._val = float(\"nan\")\n        elif denominator == 1:\n            self._val = Fraction(value)\n        else:\n            self._val = Fraction(value, denominator)\n\n    @property\n    def numerator(self):\n        return self._numerator\n\n    @property\n    def denominator(self):\n        return self._denominator\n\n    def limit_rational(self, max_denominator):\n        \"\"\"\n\n        :param max_denominator: Integer, the maximum denominator value\n        :returns: Tuple of (numerator, denominator)\n        \"\"\"\n\n        if self.denominator == 0:\n            return self.numerator, self.denominator\n\n        f = self._val.limit_denominator(max_denominator)\n        return f.numerator, f.denominator\n\n    def __repr__(self):\n        return str(float(self._val))\n\n    def __hash__(self):\n        return self._val.__hash__()\n\n    def __eq__(self, other):\n        val = self._val\n        if isinstance(other, IFDRational):\n            other = other._val\n        if isinstance(other, float):\n            val = float(val)\n        return val == other\n\n    def __getstate__(self):\n        return [self._val, self._numerator, self._denominator]\n\n    def __setstate__(self, state):\n        IFDRational.__init__(self, 0)\n        _val, _numerator, _denominator = state\n        self._val = _val\n        self._numerator = _numerator\n        self._denominator = _denominator\n\n    def _delegate(op):\n        def delegate(self, *args):\n            return getattr(self._val, op)(*args)\n\n        return delegate\n\n    \"\"\" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',\n             'truediv', 'rtruediv', 'floordiv', 'rfloordiv',\n             'mod','rmod', 'pow','rpow', 'pos', 'neg',\n             'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',\n             'ceil', 'floor', 'round']\n        print(\"\\n\".join(\"__%s__ = _delegate('__%s__')\" % (s,s) for s in a))\n        \"\"\"\n\n    __add__ = _delegate(\"__add__\")\n    __radd__ = _delegate(\"__radd__\")\n    __sub__ = _delegate(\"__sub__\")\n    __rsub__ = _delegate(\"__rsub__\")\n    __mul__ = _delegate(\"__mul__\")\n    __rmul__ = _delegate(\"__rmul__\")\n    __truediv__ = _delegate(\"__truediv__\")\n    __rtruediv__ = _delegate(\"__rtruediv__\")\n    __floordiv__ = _delegate(\"__floordiv__\")\n    __rfloordiv__ = _delegate(\"__rfloordiv__\")\n    __mod__ = _delegate(\"__mod__\")\n    __rmod__ = _delegate(\"__rmod__\")\n    __pow__ = _delegate(\"__pow__\")\n    __rpow__ = _delegate(\"__rpow__\")\n    __pos__ = _delegate(\"__pos__\")\n    __neg__ = _delegate(\"__neg__\")\n    __abs__ = _delegate(\"__abs__\")\n    __trunc__ = _delegate(\"__trunc__\")\n    __lt__ = _delegate(\"__lt__\")\n    __gt__ = _delegate(\"__gt__\")\n    __le__ = _delegate(\"__le__\")\n    __ge__ = _delegate(\"__ge__\")\n    __bool__ = _delegate(\"__bool__\")\n    __ceil__ = _delegate(\"__ceil__\")\n    __floor__ = _delegate(\"__floor__\")\n    __round__ = _delegate(\"__round__\")\n\n\nclass ImageFileDirectory_v2(MutableMapping):\n    \"\"\"This class represents a TIFF tag directory.  To speed things up, we\n    don't decode tags unless they're asked for.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v2()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        'Some Data'\n\n    Individual values are returned as the strings or numbers, sequences are\n    returned as tuples of the values.\n\n    The tiff metadata type of each item is stored in a dictionary of\n    tag types in\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types\n    are read from a tiff file, guessed from the type added, or added\n    manually.\n\n    Data Structures:\n\n        * ``self.tagtype = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: integer corresponding to the data type from\n            :py:data:`.TiffTags.TYPES`\n\n          .. versionadded:: 3.0.0\n\n    'Internal' data structures:\n\n        * ``self._tags_v2 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data, as tuple for multiple values\n\n        * ``self._tagdata = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: undecoded byte string from file\n\n        * ``self._tags_v1 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data in the v1 format\n\n    Tags will be found in the private attributes ``self._tagdata``, and in\n    ``self._tags_v2`` once decoded.\n\n    ``self.legacy_api`` is a value for internal use, and shouldn't be changed\n    from outside code. In cooperation with\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``\n    is true, then decoded tags will be populated into both ``_tags_v1`` and\n    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF\n    save routine. Tags should be read from ``_tags_v1`` if\n    ``legacy_api == true``.\n\n    \"\"\"\n\n    def __init__(self, ifh=b\"II\\052\\0\\0\\0\\0\\0\", prefix=None, group=None):\n        \"\"\"Initialize an ImageFileDirectory.\n\n        To construct an ImageFileDirectory from a real file, pass the 8-byte\n        magic header to the constructor.  To only set the endianness, pass it\n        as the 'prefix' keyword argument.\n\n        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets\n              endianness.\n        :param prefix: Override the endianness of the file.\n        \"\"\"\n        if not _accept(ifh):\n            raise SyntaxError(f\"not a TIFF file (header {repr(ifh)} not valid)\")\n        self._prefix = prefix if prefix is not None else ifh[:2]\n        if self._prefix == MM:\n            self._endian = \">\"\n        elif self._prefix == II:\n            self._endian = \"<\"\n        else:\n            raise SyntaxError(\"not a TIFF IFD\")\n        self._bigtiff = ifh[2] == 43\n        self.group = group\n        self.tagtype = {}\n        \"\"\" Dictionary of tag types \"\"\"\n        self.reset()\n        (self.next,) = (\n            self._unpack(\"Q\", ifh[8:]) if self._bigtiff else self._unpack(\"L\", ifh[4:])\n        )\n        self._legacy_api = False\n\n    prefix = property(lambda self: self._prefix)\n    offset = property(lambda self: self._offset)\n    legacy_api = property(lambda self: self._legacy_api)\n\n    @legacy_api.setter\n    def legacy_api(self, value):\n        raise Exception(\"Not allowing setting of legacy api\")\n\n    def reset(self):\n        self._tags_v1 = {}  # will remain empty if legacy_api is false\n        self._tags_v2 = {}  # main tag storage\n        self._tagdata = {}\n        self.tagtype = {}  # added 2008-06-05 by Florian Hoech\n        self._next = None\n        self._offset = None\n\n    def __str__(self):\n        return str(dict(self))\n\n    def named(self):\n        \"\"\"\n        :returns: dict of name|key: value\n\n        Returns the complete tag dictionary, with named tags where possible.\n        \"\"\"\n        return {\n            TiffTags.lookup(code, self.group).name: value\n            for code, value in self.items()\n        }\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v2))\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v2:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            self[tag] = handler(self, data, self.legacy_api)  # check type\n        val = self._tags_v2[tag]\n        if self.legacy_api and not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n    def __contains__(self, tag):\n        return tag in self._tags_v2 or tag in self._tagdata\n\n    def __setitem__(self, tag, value):\n        self._setitem(tag, value, self.legacy_api)\n\n    def _setitem(self, tag, value, legacy_api):\n        basetypes = (Number, bytes, str)\n\n        info = TiffTags.lookup(tag, self.group)\n        values = [value] if isinstance(value, basetypes) else value\n\n        if tag not in self.tagtype:\n            if info.type:\n                self.tagtype[tag] = info.type\n            else:\n                self.tagtype[tag] = TiffTags.UNDEFINED\n                if all(isinstance(v, IFDRational) for v in values):\n                    self.tagtype[tag] = (\n                        TiffTags.RATIONAL\n                        if all(v >= 0 for v in values)\n                        else TiffTags.SIGNED_RATIONAL\n                    )\n                elif all(isinstance(v, int) for v in values):\n                    if all(0 <= v < 2**16 for v in values):\n                        self.tagtype[tag] = TiffTags.SHORT\n                    elif all(-(2**15) < v < 2**15 for v in values):\n                        self.tagtype[tag] = TiffTags.SIGNED_SHORT\n                    else:\n                        self.tagtype[tag] = (\n                            TiffTags.LONG\n                            if all(v >= 0 for v in values)\n                            else TiffTags.SIGNED_LONG\n                        )\n                elif all(isinstance(v, float) for v in values):\n                    self.tagtype[tag] = TiffTags.DOUBLE\n                elif all(isinstance(v, str) for v in values):\n                    self.tagtype[tag] = TiffTags.ASCII\n                elif all(isinstance(v, bytes) for v in values):\n                    self.tagtype[tag] = TiffTags.BYTE\n\n        if self.tagtype[tag] == TiffTags.UNDEFINED:\n            values = [\n                v.encode(\"ascii\", \"replace\") if isinstance(v, str) else v\n                for v in values\n            ]\n        elif self.tagtype[tag] == TiffTags.RATIONAL:\n            values = [float(v) if isinstance(v, int) else v for v in values]\n\n        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)\n        if not is_ifd:\n            values = tuple(info.cvt_enum(value) for value in values)\n\n        dest = self._tags_v1 if legacy_api else self._tags_v2\n\n        # Three branches:\n        # Spec'd length == 1, Actual length 1, store as element\n        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.\n        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.\n        # Don't mess with the legacy api, since it's frozen.\n        if not is_ifd and (\n            (info.length == 1)\n            or self.tagtype[tag] == TiffTags.BYTE\n            or (info.length is None and len(values) == 1 and not legacy_api)\n        ):\n            # Don't mess with the legacy api, since it's frozen.\n            if legacy_api and self.tagtype[tag] in [\n                TiffTags.RATIONAL,\n                TiffTags.SIGNED_RATIONAL,\n            ]:  # rationals\n                values = (values,)\n            try:\n                (dest[tag],) = values\n            except ValueError:\n                # We've got a builtin tag with 1 expected entry\n                warnings.warn(\n                    f\"Metadata Warning, tag {tag} had too many entries: \"\n                    f\"{len(values)}, expected 1\"\n                )\n                dest[tag] = values[0]\n\n        else:\n            # Spec'd length > 1 or undefined\n            # Unspec'd, and length > 1\n            dest[tag] = values\n\n    def __delitem__(self, tag):\n        self._tags_v2.pop(tag, None)\n        self._tags_v1.pop(tag, None)\n        self._tagdata.pop(tag, None)\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v2))\n\n    def _unpack(self, fmt, data):\n        return struct.unpack(self._endian + fmt, data)\n\n    def _pack(self, fmt, *values):\n        return struct.pack(self._endian + fmt, *values)\n\n    def _register_loader(idx, size):\n        def decorator(func):\n            from .TiffTags import TYPES\n\n            if func.__name__.startswith(\"load_\"):\n                TYPES[idx] = func.__name__[5:].replace(\"_\", \" \")\n            _load_dispatch[idx] = size, func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_writer(idx):\n        def decorator(func):\n            _write_dispatch[idx] = func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_basic(idx_fmt_name):\n        from .TiffTags import TYPES\n\n        idx, fmt, name = idx_fmt_name\n        TYPES[idx] = name\n        size = struct.calcsize(\"=\" + fmt)\n        _load_dispatch[idx] = (  # noqa: F821\n            size,\n            lambda self, data, legacy_api=True: (\n                self._unpack(f\"{len(data) // size}{fmt}\", data)\n            ),\n        )\n        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821\n            b\"\".join(self._pack(fmt, value) for value in values)\n        )\n\n    list(\n        map(\n            _register_basic,\n            [\n                (TiffTags.SHORT, \"H\", \"short\"),\n                (TiffTags.LONG, \"L\", \"long\"),\n                (TiffTags.SIGNED_BYTE, \"b\", \"signed byte\"),\n                (TiffTags.SIGNED_SHORT, \"h\", \"signed short\"),\n                (TiffTags.SIGNED_LONG, \"l\", \"signed long\"),\n                (TiffTags.FLOAT, \"f\", \"float\"),\n                (TiffTags.DOUBLE, \"d\", \"double\"),\n                (TiffTags.IFD, \"L\", \"long\"),\n                (TiffTags.LONG8, \"Q\", \"long8\"),\n            ],\n        )\n    )\n\n    @_register_loader(1, 1)  # Basic type, except for the legacy API.\n    def load_byte(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(1)  # Basic type, except for the legacy API.\n    def write_byte(self, data):\n        if isinstance(data, int):\n            data = bytes((data,))\n        return data\n\n    @_register_loader(2, 1)\n    def load_string(self, data, legacy_api=True):\n        if data.endswith(b\"\\0\"):\n            data = data[:-1]\n        return data.decode(\"latin-1\", \"replace\")\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if not isinstance(value, bytes):\n            value = value.encode(\"ascii\", \"replace\")\n        return value + b\"\\0\"\n\n    @_register_loader(5, 8)\n    def load_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}L\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(5)\n    def write_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2L\", *_limit_rational(frac, 2**32 - 1)) for frac in values\n        )\n\n    @_register_loader(7, 1)\n    def load_undefined(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(7)\n    def write_undefined(self, value):\n        return value\n\n    @_register_loader(10, 8)\n    def load_signed_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}l\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(10)\n    def write_signed_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2l\", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))\n            for frac in values\n        )\n\n    def _ensure_read(self, fp, size):\n        ret = fp.read(size)\n        if len(ret) != size:\n            raise OSError(\n                \"Corrupt EXIF data.  \"\n                f\"Expecting to read {size} bytes but only got {len(ret)}. \"\n            )\n        return ret\n\n    def load(self, fp):\n\n        self.reset()\n        self._offset = fp.tell()\n\n        try:\n            tag_count = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"H\", self._ensure_read(fp, 2))\n            )[0]\n            for i in range(tag_count):\n                tag, typ, count, data = (\n                    self._unpack(\"HHQ8s\", self._ensure_read(fp, 20))\n                    if self._bigtiff\n                    else self._unpack(\"HHL4s\", self._ensure_read(fp, 12))\n                )\n\n                tagname = TiffTags.lookup(tag, self.group).name\n                typname = TYPES.get(typ, \"unknown\")\n                msg = f\"tag: {tagname} ({tag}) - type: {typname} ({typ})\"\n\n                try:\n                    unit_size, handler = self._load_dispatch[typ]\n                except KeyError:\n                    logger.debug(msg + f\" - unsupported type {typ}\")\n                    continue  # ignore unsupported type\n                size = count * unit_size\n                if size > (8 if self._bigtiff else 4):\n                    here = fp.tell()\n                    (offset,) = self._unpack(\"Q\" if self._bigtiff else \"L\", data)\n                    msg += f\" Tag Location: {here} - Data Location: {offset}\"\n                    fp.seek(offset)\n                    data = ImageFile._safe_read(fp, size)\n                    fp.seek(here)\n                else:\n                    data = data[:size]\n\n                if len(data) != size:\n                    warnings.warn(\n                        \"Possibly corrupt EXIF data.  \"\n                        f\"Expecting to read {size} bytes but only got {len(data)}.\"\n                        f\" Skipping tag {tag}\"\n                    )\n                    logger.debug(msg)\n                    continue\n\n                if not data:\n                    logger.debug(msg)\n                    continue\n\n                self._tagdata[tag] = data\n                self.tagtype[tag] = typ\n\n                msg += \" - value: \" + (\n                    \"<table: %d bytes>\" % size if size > 32 else repr(data)\n                )\n                logger.debug(msg)\n\n            (self.next,) = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"L\", self._ensure_read(fp, 4))\n            )\n        except OSError as msg:\n            warnings.warn(str(msg))\n            return\n\n    def tobytes(self, offset=0):\n        # FIXME What about tagdata?\n        result = self._pack(\"H\", len(self._tags_v2))\n\n        entries = []\n        offset = offset + len(result) + len(self._tags_v2) * 12 + 4\n        stripoffsets = None\n\n        # pass 1: convert tags to binary format\n        # always write tags in ascending order\n        for tag, value in sorted(self._tags_v2.items()):\n            if tag == STRIPOFFSETS:\n                stripoffsets = len(entries)\n            typ = self.tagtype.get(tag)\n            logger.debug(f\"Tag {tag}, Type: {typ}, Value: {repr(value)}\")\n            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)\n            if is_ifd:\n                if self._endian == \"<\":\n                    ifh = b\"II\\x2A\\x00\\x08\\x00\\x00\\x00\"\n                else:\n                    ifh = b\"MM\\x00\\x2A\\x00\\x00\\x00\\x08\"\n                ifd = ImageFileDirectory_v2(ifh, group=tag)\n                values = self._tags_v2[tag]\n                for ifd_tag, ifd_value in values.items():\n                    ifd[ifd_tag] = ifd_value\n                data = ifd.tobytes(offset)\n            else:\n                values = value if isinstance(value, tuple) else (value,)\n                data = self._write_dispatch[typ](self, *values)\n\n            tagname = TiffTags.lookup(tag, self.group).name\n            typname = \"ifd\" if is_ifd else TYPES.get(typ, \"unknown\")\n            msg = f\"save: {tagname} ({tag}) - type: {typname} ({typ})\"\n            msg += \" - value: \" + (\n                \"<table: %d bytes>\" % len(data) if len(data) >= 16 else str(values)\n            )\n            logger.debug(msg)\n\n            # count is sum of lengths for string and arbitrary data\n            if is_ifd:\n                count = 1\n            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:\n                count = len(data)\n            else:\n                count = len(values)\n            # figure out if data fits into the entry\n            if len(data) <= 4:\n                entries.append((tag, typ, count, data.ljust(4, b\"\\0\"), b\"\"))\n            else:\n                entries.append((tag, typ, count, self._pack(\"L\", offset), data))\n                offset += (len(data) + 1) // 2 * 2  # pad to word\n\n        # update strip offset data to point beyond auxiliary data\n        if stripoffsets is not None:\n            tag, typ, count, value, data = entries[stripoffsets]\n            if data:\n                raise NotImplementedError(\"multistrip support not yet implemented\")\n            value = self._pack(\"L\", self._unpack(\"L\", value)[0] + offset)\n            entries[stripoffsets] = tag, typ, count, value, data\n\n        # pass 2: write entries to file\n        for tag, typ, count, value, data in entries:\n            logger.debug(f\"{tag} {typ} {count} {repr(value)} {repr(data)}\")\n            result += self._pack(\"HHL4s\", tag, typ, count, value)\n\n        # -- overwrite here for multi-page --\n        result += b\"\\0\\0\\0\\0\"  # end of entries\n\n        # pass 3: write auxiliary data to file\n        for tag, typ, count, value, data in entries:\n            result += data\n            if len(data) & 1:\n                result += b\"\\0\"\n\n        return result\n\n    def save(self, fp):\n\n        if fp.tell() == 0:  # skip TIFF header on subsequent pages\n            # tiff header -- PIL always starts the first IFD at offset 8\n            fp.write(self._prefix + self._pack(\"HL\", 42, 8))\n\n        offset = fp.tell()\n        result = self.tobytes(offset)\n        fp.write(result)\n        return offset + len(result)\n\n\nImageFileDirectory_v2._load_dispatch = _load_dispatch\nImageFileDirectory_v2._write_dispatch = _write_dispatch\nfor idx, name in TYPES.items():\n    name = name.replace(\" \", \"_\")\n    setattr(ImageFileDirectory_v2, \"load_\" + name, _load_dispatch[idx][1])\n    setattr(ImageFileDirectory_v2, \"write_\" + name, _write_dispatch[idx])\ndel _load_dispatch, _write_dispatch, idx, name\n\n\n# Legacy ImageFileDirectory support.\nclass ImageFileDirectory_v1(ImageFileDirectory_v2):\n    \"\"\"This class represents the **legacy** interface to a TIFF tag directory.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v1()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        ('Some Data',)\n\n    Also contains a dictionary of tag types as read from the tiff image file,\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.\n\n    Values are returned as a tuple.\n\n    ..  deprecated:: 3.0.0\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._legacy_api = True\n\n    tags = property(lambda self: self._tags_v1)\n    tagdata = property(lambda self: self._tagdata)\n\n    # defined in ImageFileDirectory_v2\n    tagtype: dict\n    \"\"\"Dictionary of tag types\"\"\"\n\n    @classmethod\n    def from_v2(cls, original):\n        \"\"\"Returns an\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n        instance with the same data as is contained in the original\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n        instance.\n\n        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n\n        \"\"\"\n\n        ifd = cls(prefix=original.prefix)\n        ifd._tagdata = original._tagdata\n        ifd.tagtype = original.tagtype\n        ifd.next = original.next  # an indicator for multipage tiffs\n        return ifd\n\n    def to_v2(self):\n        \"\"\"Returns an\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n        instance with the same data as is contained in the original\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n        instance.\n\n        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n\n        \"\"\"\n\n        ifd = ImageFileDirectory_v2(prefix=self.prefix)\n        ifd._tagdata = dict(self._tagdata)\n        ifd.tagtype = dict(self.tagtype)\n        ifd._tags_v2 = dict(self._tags_v2)\n        return ifd\n\n    def __contains__(self, tag):\n        return tag in self._tags_v1 or tag in self._tagdata\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v1))\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v1))\n\n    def __setitem__(self, tag, value):\n        for legacy_api in (False, True):\n            self._setitem(tag, value, legacy_api)\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v1:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            for legacy in (False, True):\n                self._setitem(tag, handler(self, data, legacy), legacy)\n        val = self._tags_v1[tag]\n        if not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n\n# undone -- switch this pointer when IFD_LEGACY_API == False\nImageFileDirectory = ImageFileDirectory_v1\n\n\n##\n# Image plugin for TIFF files.\n\n\nclass TiffImageFile(ImageFile.ImageFile):\n\n    format = \"TIFF\"\n    format_description = \"Adobe TIFF\"\n    _close_exclusive_fp_after_loading = False\n\n    def __init__(self, fp=None, filename=None):\n        self.tag_v2 = None\n        \"\"\" Image file directory (tag dictionary) \"\"\"\n\n        self.tag = None\n        \"\"\" Legacy tag entries \"\"\"\n\n        super().__init__(fp, filename)\n\n    def _open(self):\n        \"\"\"Open the first image in a TIFF file\"\"\"\n\n        # Header\n        ifh = self.fp.read(8)\n        if ifh[2] == 43:\n            ifh += self.fp.read(8)\n\n        self.tag_v2 = ImageFileDirectory_v2(ifh)\n\n        # legacy IFD entries will be filled in later\n        self.ifd = None\n\n        # setup frame pointers\n        self.__first = self.__next = self.tag_v2.next\n        self.__frame = -1\n        self._fp = self.fp\n        self._frame_pos = []\n        self._n_frames = None\n\n        logger.debug(\"*** TiffImageFile._open ***\")\n        logger.debug(f\"- __first: {self.__first}\")\n        logger.debug(f\"- ifh: {repr(ifh)}\")  # Use repr to avoid str(bytes)\n\n        # and load the first frame\n        self._seek(0)\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            self._seek(len(self._frame_pos))\n            while self._n_frames is None:\n                self._seek(self.tell() + 1)\n            self.seek(current)\n        return self._n_frames\n\n    def seek(self, frame):\n        \"\"\"Select a given frame as current image\"\"\"\n        if not self._seek_check(frame):\n            return\n        self._seek(frame)\n        # Create a new core image object on second and\n        # subsequent frames in the image. Image may be\n        # different size/mode.\n        Image._decompression_bomb_check(self.size)\n        self.im = Image.core.new(self.mode, self.size)\n\n    def _seek(self, frame):\n        self.fp = self._fp\n\n        # reset buffered io handle in case fp\n        # was passed to libtiff, invalidating the buffer\n        self.fp.tell()\n\n        while len(self._frame_pos) <= frame:\n            if not self.__next:\n                raise EOFError(\"no more images in TIFF file\")\n            logger.debug(\n                f\"Seeking to frame {frame}, on frame {self.__frame}, \"\n                f\"__next {self.__next}, location: {self.fp.tell()}\"\n            )\n            self.fp.seek(self.__next)\n            self._frame_pos.append(self.__next)\n            logger.debug(\"Loading tags, location: %s\" % self.fp.tell())\n            self.tag_v2.load(self.fp)\n            if self.tag_v2.next in self._frame_pos:\n                # This IFD has already been processed\n                # Declare this to be the end of the image\n                self.__next = 0\n            else:\n                self.__next = self.tag_v2.next\n            if self.__next == 0:\n                self._n_frames = frame + 1\n            if len(self._frame_pos) == 1:\n                self.is_animated = self.__next != 0\n            self.__frame += 1\n        self.fp.seek(self._frame_pos[frame])\n        self.tag_v2.load(self.fp)\n        self._reload_exif()\n        # fill the legacy tag/ifd entries\n        self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)\n        self.__frame = frame\n        self._setup()\n\n    def tell(self):\n        \"\"\"Return the current frame number\"\"\"\n        return self.__frame\n\n    def get_child_images(self):\n        if SUBIFD not in self.tag_v2:\n            return []\n        child_images = []\n        exif = self.getexif()\n        offset = None\n        for im_offset in self.tag_v2[SUBIFD]:\n            # reset buffered io handle in case fp\n            # was passed to libtiff, invalidating the buffer\n            current_offset = self._fp.tell()\n            if offset is None:\n                offset = current_offset\n\n            fp = self._fp\n            ifd = exif._get_ifd_dict(im_offset)\n            jpegInterchangeFormat = ifd.get(513)\n            if jpegInterchangeFormat is not None:\n                fp.seek(jpegInterchangeFormat)\n                jpeg_data = fp.read(ifd.get(514))\n\n                fp = io.BytesIO(jpeg_data)\n\n            with Image.open(fp) as im:\n                if jpegInterchangeFormat is None:\n                    im._frame_pos = [im_offset]\n                    im._seek(0)\n                im.load()\n                child_images.append(im)\n\n        if offset is not None:\n            self._fp.seek(offset)\n        return child_images\n\n    def getxmp(self):\n        \"\"\"\n        Returns a dictionary containing the XMP tags.\n        Requires defusedxml to be installed.\n\n        :returns: XMP tags in a dictionary.\n        \"\"\"\n        return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}\n\n    def get_photoshop_blocks(self):\n        \"\"\"\n        Returns a dictionary of Photoshop \"Image Resource Blocks\".\n        The keys are the image resource ID. For more information, see\n        https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727\n\n        :returns: Photoshop \"Image Resource Blocks\" in a dictionary.\n        \"\"\"\n        blocks = {}\n        val = self.tag_v2.get(0x8649)\n        if val:\n            while val[:4] == b\"8BIM\":\n                id = i16(val[4:6])\n                n = math.ceil((val[6] + 1) / 2) * 2\n                size = i32(val[6 + n : 10 + n])\n                data = val[10 + n : 10 + n + size]\n                blocks[id] = {\"data\": data}\n\n                val = val[math.ceil((10 + n + size) / 2) * 2 :]\n        return blocks\n\n    def load(self):\n        if self.tile and self.use_load_libtiff:\n            return self._load_libtiff()\n        return super().load()\n\n    def load_end(self):\n        if self._tile_orientation:\n            method = {\n                2: Image.Transpose.FLIP_LEFT_RIGHT,\n                3: Image.Transpose.ROTATE_180,\n                4: Image.Transpose.FLIP_TOP_BOTTOM,\n                5: Image.Transpose.TRANSPOSE,\n                6: Image.Transpose.ROTATE_270,\n                7: Image.Transpose.TRANSVERSE,\n                8: Image.Transpose.ROTATE_90,\n            }.get(self._tile_orientation)\n            if method is not None:\n                self.im = self.im.transpose(method)\n                self._size = self.im.size\n\n        # allow closing if we're on the first frame, there's no next\n        # This is the ImageFile.load path only, libtiff specific below.\n        if not self.is_animated:\n            self._close_exclusive_fp_after_loading = True\n\n            # reset buffered io handle in case fp\n            # was passed to libtiff, invalidating the buffer\n            self.fp.tell()\n\n            # load IFD data from fp before it is closed\n            exif = self.getexif()\n            for key in TiffTags.TAGS_V2_GROUPS.keys():\n                if key not in exif:\n                    continue\n                exif.get_ifd(key)\n\n    def _load_libtiff(self):\n        \"\"\"Overload method triggered when we detect a compressed tiff\n        Calls out to libtiff\"\"\"\n\n        Image.Image.load(self)\n\n        self.load_prepare()\n\n        if not len(self.tile) == 1:\n            raise OSError(\"Not exactly one tile\")\n\n        # (self._compression, (extents tuple),\n        #   0, (rawmode, self._compression, fp))\n        extents = self.tile[0][1]\n        args = list(self.tile[0][3])\n\n        # To be nice on memory footprint, if there's a\n        # file descriptor, use that instead of reading\n        # into a string in python.\n        # libtiff closes the file descriptor, so pass in a dup.\n        try:\n            fp = hasattr(self.fp, \"fileno\") and os.dup(self.fp.fileno())\n            # flush the file descriptor, prevents error on pypy 2.4+\n            # should also eliminate the need for fp.tell\n            # in _seek\n            if hasattr(self.fp, \"flush\"):\n                self.fp.flush()\n        except OSError:\n            # io.BytesIO have a fileno, but returns an OSError if\n            # it doesn't use a file descriptor.\n            fp = False\n\n        if fp:\n            args[2] = fp\n\n        decoder = Image._getdecoder(\n            self.mode, \"libtiff\", tuple(args), self.decoderconfig\n        )\n        try:\n            decoder.setimage(self.im, extents)\n        except ValueError as e:\n            raise OSError(\"Couldn't set the image\") from e\n\n        close_self_fp = self._exclusive_fp and not self.is_animated\n        if hasattr(self.fp, \"getvalue\"):\n            # We've got a stringio like thing passed in. Yay for all in memory.\n            # The decoder needs the entire file in one shot, so there's not\n            # a lot we can do here other than give it the entire file.\n            # unless we could do something like get the address of the\n            # underlying string for stringio.\n            #\n            # Rearranging for supporting byteio items, since they have a fileno\n            # that returns an OSError if there's no underlying fp. Easier to\n            # deal with here by reordering.\n            logger.debug(\"have getvalue. just sending in a string from getvalue\")\n            n, err = decoder.decode(self.fp.getvalue())\n        elif fp:\n            # we've got a actual file on disk, pass in the fp.\n            logger.debug(\"have fileno, calling fileno version of the decoder.\")\n            if not close_self_fp:\n                self.fp.seek(0)\n            # 4 bytes, otherwise the trace might error out\n            n, err = decoder.decode(b\"fpfp\")\n        else:\n            # we have something else.\n            logger.debug(\"don't have fileno or getvalue. just reading\")\n            self.fp.seek(0)\n            # UNDONE -- so much for that buffer size thing.\n            n, err = decoder.decode(self.fp.read())\n\n        if fp:\n            try:\n                os.close(fp)\n            except OSError:\n                pass\n\n        self.tile = []\n        self.readonly = 0\n\n        self.load_end()\n\n        # libtiff closed the fp in a, we need to close self.fp, if possible\n        if close_self_fp:\n            self.fp.close()\n            self.fp = None  # might be shared\n\n        if err < 0:\n            raise OSError(err)\n\n        return Image.Image.load(self)\n\n    def _setup(self):\n        \"\"\"Setup this image object based on current tags\"\"\"\n\n        if 0xBC01 in self.tag_v2:\n            raise OSError(\"Windows Media Photo files not yet supported\")\n\n        # extract relevant tags\n        self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]\n        self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)\n\n        # photometric is a required tag, but not everyone is reading\n        # the specification\n        photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)\n\n        # old style jpeg compression images most certainly are YCbCr\n        if self._compression == \"tiff_jpeg\":\n            photo = 6\n\n        fillorder = self.tag_v2.get(FILLORDER, 1)\n\n        logger.debug(\"*** Summary ***\")\n        logger.debug(f\"- compression: {self._compression}\")\n        logger.debug(f\"- photometric_interpretation: {photo}\")\n        logger.debug(f\"- planar_configuration: {self._planar_configuration}\")\n        logger.debug(f\"- fill_order: {fillorder}\")\n        logger.debug(f\"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}\")\n\n        # size\n        xsize = int(self.tag_v2.get(IMAGEWIDTH))\n        ysize = int(self.tag_v2.get(IMAGELENGTH))\n        self._size = xsize, ysize\n\n        logger.debug(f\"- size: {self.size}\")\n\n        sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))\n        if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:\n            # SAMPLEFORMAT is properly per band, so an RGB image will\n            # be (1,1,1).  But, we don't support per band pixel types,\n            # and anything more than one band is a uint8. So, just\n            # take the first element. Revisit this if adding support\n            # for more exotic images.\n            sample_format = (1,)\n\n        bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))\n        extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())\n        if photo in (2, 6, 8):  # RGB, YCbCr, LAB\n            bps_count = 3\n        elif photo == 5:  # CMYK\n            bps_count = 4\n        else:\n            bps_count = 1\n        bps_count += len(extra_tuple)\n        bps_actual_count = len(bps_tuple)\n        samples_per_pixel = self.tag_v2.get(\n            SAMPLESPERPIXEL,\n            3 if self._compression == \"tiff_jpeg\" and photo in (2, 6) else 1,\n        )\n\n        if samples_per_pixel > MAX_SAMPLESPERPIXEL:\n            # DOS check, samples_per_pixel can be a Long, and we extend the tuple below\n            logger.error(\n                \"More samples per pixel than can be decoded: %s\", samples_per_pixel\n            )\n            raise SyntaxError(\"Invalid value for samples per pixel\")\n\n        if samples_per_pixel < bps_actual_count:\n            # If a file has more values in bps_tuple than expected,\n            # remove the excess.\n            bps_tuple = bps_tuple[:samples_per_pixel]\n        elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:\n            # If a file has only one value in bps_tuple, when it should have more,\n            # presume it is the same number of bits for all of the samples.\n            bps_tuple = bps_tuple * samples_per_pixel\n\n        if len(bps_tuple) != samples_per_pixel:\n            raise SyntaxError(\"unknown data organization\")\n\n        # mode: check photometric interpretation and bits per pixel\n        key = (\n            self.tag_v2.prefix,\n            photo,\n            sample_format,\n            fillorder,\n            bps_tuple,\n            extra_tuple,\n        )\n        logger.debug(f\"format key: {key}\")\n        try:\n            self.mode, rawmode = OPEN_INFO[key]\n        except KeyError as e:\n            logger.debug(\"- unsupported format\")\n            raise SyntaxError(\"unknown pixel mode\") from e\n\n        logger.debug(f\"- raw mode: {rawmode}\")\n        logger.debug(f\"- pil mode: {self.mode}\")\n\n        self.info[\"compression\"] = self._compression\n\n        xres = self.tag_v2.get(X_RESOLUTION, 1)\n        yres = self.tag_v2.get(Y_RESOLUTION, 1)\n\n        if xres and yres:\n            resunit = self.tag_v2.get(RESOLUTION_UNIT)\n            if resunit == 2:  # dots per inch\n                self.info[\"dpi\"] = (xres, yres)\n            elif resunit == 3:  # dots per centimeter. convert to dpi\n                self.info[\"dpi\"] = (xres * 2.54, yres * 2.54)\n            elif resunit is None:  # used to default to 1, but now 2)\n                self.info[\"dpi\"] = (xres, yres)\n                # For backward compatibility,\n                # we also preserve the old behavior\n                self.info[\"resolution\"] = xres, yres\n            else:  # No absolute unit of measurement\n                self.info[\"resolution\"] = xres, yres\n\n        # build tile descriptors\n        x = y = layer = 0\n        self.tile = []\n        self.use_load_libtiff = READ_LIBTIFF or self._compression != \"raw\"\n        if self.use_load_libtiff:\n            # Decoder expects entire file as one tile.\n            # There's a buffer size limit in load (64k)\n            # so large g4 images will fail if we use that\n            # function.\n            #\n            # Setup the one tile for the whole image, then\n            # use the _load_libtiff function.\n\n            # libtiff handles the fillmode for us, so 1;IR should\n            # actually be 1;I. Including the R double reverses the\n            # bits, so stripes of the image are reversed.  See\n            # https://github.com/python-pillow/Pillow/issues/279\n            if fillorder == 2:\n                # Replace fillorder with fillorder=1\n                key = key[:3] + (1,) + key[4:]\n                logger.debug(f\"format key: {key}\")\n                # this should always work, since all the\n                # fillorder==2 modes have a corresponding\n                # fillorder=1 mode\n                self.mode, rawmode = OPEN_INFO[key]\n            # libtiff always returns the bytes in native order.\n            # we're expecting image byte order. So, if the rawmode\n            # contains I;16, we need to convert from native to image\n            # byte order.\n            if rawmode == \"I;16\":\n                rawmode = \"I;16N\"\n            if \";16B\" in rawmode:\n                rawmode = rawmode.replace(\";16B\", \";16N\")\n            if \";16L\" in rawmode:\n                rawmode = rawmode.replace(\";16L\", \";16N\")\n\n            # YCbCr images with new jpeg compression with pixels in one plane\n            # unpacked straight into RGB values\n            if (\n                photo == 6\n                and self._compression == \"jpeg\"\n                and self._planar_configuration == 1\n            ):\n                rawmode = \"RGB\"\n\n            # Offset in the tile tuple is 0, we go from 0,0 to\n            # w,h, and we only do this once -- eds\n            a = (rawmode, self._compression, False, self.tag_v2.offset)\n            self.tile.append((\"libtiff\", (0, 0, xsize, ysize), 0, a))\n\n        elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:\n            # striped image\n            if STRIPOFFSETS in self.tag_v2:\n                offsets = self.tag_v2[STRIPOFFSETS]\n                h = self.tag_v2.get(ROWSPERSTRIP, ysize)\n                w = self.size[0]\n            else:\n                # tiled image\n                offsets = self.tag_v2[TILEOFFSETS]\n                w = self.tag_v2.get(TILEWIDTH)\n                h = self.tag_v2.get(TILELENGTH)\n\n            for offset in offsets:\n                if x + w > xsize:\n                    stride = w * sum(bps_tuple) / 8  # bytes per line\n                else:\n                    stride = 0\n\n                tile_rawmode = rawmode\n                if self._planar_configuration == 2:\n                    # each band on it's own layer\n                    tile_rawmode = rawmode[layer]\n                    # adjust stride width accordingly\n                    stride /= bps_count\n\n                a = (tile_rawmode, int(stride), 1)\n                self.tile.append(\n                    (\n                        self._compression,\n                        (x, y, min(x + w, xsize), min(y + h, ysize)),\n                        offset,\n                        a,\n                    )\n                )\n                x = x + w\n                if x >= self.size[0]:\n                    x, y = 0, y + h\n                    if y >= self.size[1]:\n                        x = y = 0\n                        layer += 1\n        else:\n            logger.debug(\"- unsupported data organization\")\n            raise SyntaxError(\"unknown data organization\")\n\n        # Fix up info.\n        if ICCPROFILE in self.tag_v2:\n            self.info[\"icc_profile\"] = self.tag_v2[ICCPROFILE]\n\n        # fixup palette descriptor\n\n        if self.mode in [\"P\", \"PA\"]:\n            palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]\n            self.palette = ImagePalette.raw(\"RGB;L\", b\"\".join(palette))\n\n        self._tile_orientation = self.tag_v2.get(0x0112)\n\n\n#\n# --------------------------------------------------------------------\n# Write TIFF files\n\n# little endian is default except for image modes with\n# explicit big endian byte-order\n\nSAVE_INFO = {\n    # mode => rawmode, byteorder, photometrics,\n    #           sampleformat, bitspersample, extra\n    \"1\": (\"1\", II, 1, 1, (1,), None),\n    \"L\": (\"L\", II, 1, 1, (8,), None),\n    \"LA\": (\"LA\", II, 1, 1, (8, 8), 2),\n    \"P\": (\"P\", II, 3, 1, (8,), None),\n    \"PA\": (\"PA\", II, 3, 1, (8, 8), 2),\n    \"I\": (\"I;32S\", II, 1, 2, (32,), None),\n    \"I;16\": (\"I;16\", II, 1, 1, (16,), None),\n    \"I;16S\": (\"I;16S\", II, 1, 2, (16,), None),\n    \"F\": (\"F;32F\", II, 1, 3, (32,), None),\n    \"RGB\": (\"RGB\", II, 2, 1, (8, 8, 8), None),\n    \"RGBX\": (\"RGBX\", II, 2, 1, (8, 8, 8, 8), 0),\n    \"RGBA\": (\"RGBA\", II, 2, 1, (8, 8, 8, 8), 2),\n    \"CMYK\": (\"CMYK\", II, 5, 1, (8, 8, 8, 8), None),\n    \"YCbCr\": (\"YCbCr\", II, 6, 1, (8, 8, 8), None),\n    \"LAB\": (\"LAB\", II, 8, 1, (8, 8, 8), None),\n    \"I;32BS\": (\"I;32BS\", MM, 1, 2, (32,), None),\n    \"I;16B\": (\"I;16B\", MM, 1, 1, (16,), None),\n    \"I;16BS\": (\"I;16BS\", MM, 1, 2, (16,), None),\n    \"F;32BF\": (\"F;32BF\", MM, 1, 3, (32,), None),\n}\n\n\ndef _save(im, fp, filename):\n\n    try:\n        rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]\n    except KeyError as e:\n        raise OSError(f\"cannot write mode {im.mode} as TIFF\") from e\n\n    ifd = ImageFileDirectory_v2(prefix=prefix)\n\n    encoderinfo = im.encoderinfo\n    encoderconfig = im.encoderconfig\n    try:\n        compression = encoderinfo[\"compression\"]\n    except KeyError:\n        compression = im.info.get(\"compression\")\n        if isinstance(compression, int):\n            # compression value may be from BMP. Ignore it\n            compression = None\n    if compression is None:\n        compression = \"raw\"\n    elif compression == \"tiff_jpeg\":\n        # OJPEG is obsolete, so use new-style JPEG compression instead\n        compression = \"jpeg\"\n    elif compression == \"tiff_deflate\":\n        compression = \"tiff_adobe_deflate\"\n\n    libtiff = WRITE_LIBTIFF or compression != \"raw\"\n\n    # required for color libtiff images\n    ifd[PLANAR_CONFIGURATION] = 1\n\n    ifd[IMAGEWIDTH] = im.size[0]\n    ifd[IMAGELENGTH] = im.size[1]\n\n    # write any arbitrary tags passed in as an ImageFileDirectory\n    if \"tiffinfo\" in encoderinfo:\n        info = encoderinfo[\"tiffinfo\"]\n    elif \"exif\" in encoderinfo:\n        info = encoderinfo[\"exif\"]\n        if isinstance(info, bytes):\n            exif = Image.Exif()\n            exif.load(info)\n            info = exif\n    else:\n        info = {}\n    logger.debug(\"Tiffinfo Keys: %s\" % list(info))\n    if isinstance(info, ImageFileDirectory_v1):\n        info = info.to_v2()\n    for key in info:\n        if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():\n            ifd[key] = info.get_ifd(key)\n        else:\n            ifd[key] = info.get(key)\n        try:\n            ifd.tagtype[key] = info.tagtype[key]\n        except Exception:\n            pass  # might not be an IFD. Might not have populated type\n\n    # additions written by Greg Couch, gregc@cgl.ucsf.edu\n    # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com\n    if hasattr(im, \"tag_v2\"):\n        # preserve tags from original TIFF image file\n        for key in (\n            RESOLUTION_UNIT,\n            X_RESOLUTION,\n            Y_RESOLUTION,\n            IPTC_NAA_CHUNK,\n            PHOTOSHOP_CHUNK,\n            XMP,\n        ):\n            if key in im.tag_v2:\n                ifd[key] = im.tag_v2[key]\n                ifd.tagtype[key] = im.tag_v2.tagtype[key]\n\n    # preserve ICC profile (should also work when saving other formats\n    # which support profiles as TIFF) -- 2008-06-06 Florian Hoech\n    icc = encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        ifd[ICCPROFILE] = icc\n\n    for key, name in [\n        (IMAGEDESCRIPTION, \"description\"),\n        (X_RESOLUTION, \"resolution\"),\n        (Y_RESOLUTION, \"resolution\"),\n        (X_RESOLUTION, \"x_resolution\"),\n        (Y_RESOLUTION, \"y_resolution\"),\n        (RESOLUTION_UNIT, \"resolution_unit\"),\n        (SOFTWARE, \"software\"),\n        (DATE_TIME, \"date_time\"),\n        (ARTIST, \"artist\"),\n        (COPYRIGHT, \"copyright\"),\n    ]:\n        if name in encoderinfo:\n            ifd[key] = encoderinfo[name]\n\n    dpi = encoderinfo.get(\"dpi\")\n    if dpi:\n        ifd[RESOLUTION_UNIT] = 2\n        ifd[X_RESOLUTION] = dpi[0]\n        ifd[Y_RESOLUTION] = dpi[1]\n\n    if bits != (1,):\n        ifd[BITSPERSAMPLE] = bits\n        if len(bits) != 1:\n            ifd[SAMPLESPERPIXEL] = len(bits)\n    if extra is not None:\n        ifd[EXTRASAMPLES] = extra\n    if format != 1:\n        ifd[SAMPLEFORMAT] = format\n\n    if PHOTOMETRIC_INTERPRETATION not in ifd:\n        ifd[PHOTOMETRIC_INTERPRETATION] = photo\n    elif im.mode in (\"1\", \"L\") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:\n        if im.mode == \"1\":\n            inverted_im = im.copy()\n            px = inverted_im.load()\n            for y in range(inverted_im.height):\n                for x in range(inverted_im.width):\n                    px[x, y] = 0 if px[x, y] == 255 else 255\n            im = inverted_im\n        else:\n            im = ImageOps.invert(im)\n\n    if im.mode in [\"P\", \"PA\"]:\n        lut = im.im.getpalette(\"RGB\", \"RGB;L\")\n        colormap = []\n        colors = len(lut) // 3\n        for i in range(3):\n            colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]\n            colormap += [0] * (256 - colors)\n        ifd[COLORMAP] = colormap\n    # data orientation\n    stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)\n    # aim for given strip size (64 KB by default) when using libtiff writer\n    if libtiff:\n        im_strip_size = encoderinfo.get(\"strip_size\", STRIP_SIZE)\n        rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])\n        # JPEG encoder expects multiple of 8 rows\n        if compression == \"jpeg\":\n            rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])\n    else:\n        rows_per_strip = im.size[1]\n    if rows_per_strip == 0:\n        rows_per_strip = 1\n    strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip\n    strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip\n    ifd[ROWSPERSTRIP] = rows_per_strip\n    if strip_byte_counts >= 2**16:\n        ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG\n    ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (\n        stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),\n    )\n    ifd[STRIPOFFSETS] = tuple(\n        range(0, strip_byte_counts * strips_per_image, strip_byte_counts)\n    )  # this is adjusted by IFD writer\n    # no compression by default:\n    ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)\n\n    if im.mode == \"YCbCr\":\n        for tag, value in {\n            YCBCRSUBSAMPLING: (1, 1),\n            REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),\n        }.items():\n            ifd.setdefault(tag, value)\n\n    blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]\n    if libtiff:\n        if \"quality\" in encoderinfo:\n            quality = encoderinfo[\"quality\"]\n            if not isinstance(quality, int) or quality < 0 or quality > 100:\n                raise ValueError(\"Invalid quality setting\")\n            if compression != \"jpeg\":\n                raise ValueError(\n                    \"quality setting only supported for 'jpeg' compression\"\n                )\n            ifd[JPEGQUALITY] = quality\n\n        logger.debug(\"Saving using libtiff encoder\")\n        logger.debug(\"Items: %s\" % sorted(ifd.items()))\n        _fp = 0\n        if hasattr(fp, \"fileno\"):\n            try:\n                fp.seek(0)\n                _fp = os.dup(fp.fileno())\n            except io.UnsupportedOperation:\n                pass\n\n        # optional types for non core tags\n        types = {}\n        # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library\n        # based on the data in the strip.\n        # The other tags expect arrays with a certain length (fixed or depending on\n        # BITSPERSAMPLE, etc), passing arrays with a different length will result in\n        # segfaults. Block these tags until we add extra validation.\n        # SUBIFD may also cause a segfault.\n        blocklist += [\n            REFERENCEBLACKWHITE,\n            STRIPBYTECOUNTS,\n            STRIPOFFSETS,\n            TRANSFERFUNCTION,\n            SUBIFD,\n        ]\n\n        # bits per sample is a single short in the tiff directory, not a list.\n        atts = {BITSPERSAMPLE: bits[0]}\n        # Merge the ones that we have with (optional) more bits from\n        # the original file, e.g x,y resolution so that we can\n        # save(load('')) == original file.\n        legacy_ifd = {}\n        if hasattr(im, \"tag\"):\n            legacy_ifd = im.tag.to_v2()\n\n        # SAMPLEFORMAT is determined by the image format and should not be copied\n        # from legacy_ifd.\n        supplied_tags = {**getattr(im, \"tag_v2\", {}), **legacy_ifd}\n        if SAMPLEFORMAT in supplied_tags:\n            del supplied_tags[SAMPLEFORMAT]\n\n        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):\n            # Libtiff can only process certain core items without adding\n            # them to the custom dictionary.\n            # Custom items are supported for int, float, unicode, string and byte\n            # values. Other types and tuples require a tagtype.\n            if tag not in TiffTags.LIBTIFF_CORE:\n                if not Image.core.libtiff_support_custom_tags:\n                    continue\n\n                if tag in ifd.tagtype:\n                    types[tag] = ifd.tagtype[tag]\n                elif not (isinstance(value, (int, float, str, bytes))):\n                    continue\n                else:\n                    type = TiffTags.lookup(tag).type\n                    if type:\n                        types[tag] = type\n            if tag not in atts and tag not in blocklist:\n                if isinstance(value, str):\n                    atts[tag] = value.encode(\"ascii\", \"replace\") + b\"\\0\"\n                elif isinstance(value, IFDRational):\n                    atts[tag] = float(value)\n                else:\n                    atts[tag] = value\n\n        if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:\n            atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]\n\n        logger.debug(\"Converted items: %s\" % sorted(atts.items()))\n\n        # libtiff always expects the bytes in native order.\n        # we're storing image byte order. So, if the rawmode\n        # contains I;16, we need to convert from native to image\n        # byte order.\n        if im.mode in (\"I;16B\", \"I;16\"):\n            rawmode = \"I;16N\"\n\n        # Pass tags as sorted list so that the tags are set in a fixed order.\n        # This is required by libtiff for some tags. For example, the JPEGQUALITY\n        # pseudo tag requires that the COMPRESS tag was already set.\n        tags = list(atts.items())\n        tags.sort()\n        a = (rawmode, compression, _fp, filename, tags, types)\n        e = Image._getencoder(im.mode, \"libtiff\", a, encoderconfig)\n        e.setimage(im.im, (0, 0) + im.size)\n        while True:\n            # undone, change to self.decodermaxblock:\n            l, s, d = e.encode(16 * 1024)\n            if not _fp:\n                fp.write(d)\n            if s:\n                break\n        if s < 0:\n            raise OSError(f\"encoder error {s} when writing image file\")\n\n    else:\n        for tag in blocklist:\n            del ifd[tag]\n        offset = ifd.save(fp)\n\n        ImageFile._save(\n            im, fp, [(\"raw\", (0, 0) + im.size, offset, (rawmode, stride, 1))]\n        )\n\n    # -- helper for multi-page save --\n    if \"_debug_multipage\" in encoderinfo:\n        # just to access o32 and o16 (using correct byte order)\n        im._debug_multipage = ifd\n\n\nclass AppendingTiffWriter:\n    fieldSizes = [\n        0,  # None\n        1,  # byte\n        1,  # ascii\n        2,  # short\n        4,  # long\n        8,  # rational\n        1,  # sbyte\n        1,  # undefined\n        2,  # sshort\n        4,  # slong\n        8,  # srational\n        4,  # float\n        8,  # double\n    ]\n\n    #    StripOffsets = 273\n    #    FreeOffsets = 288\n    #    TileOffsets = 324\n    #    JPEGQTables = 519\n    #    JPEGDCTables = 520\n    #    JPEGACTables = 521\n    Tags = {273, 288, 324, 519, 520, 521}\n\n    def __init__(self, fn, new=False):\n        if hasattr(fn, \"read\"):\n            self.f = fn\n            self.close_fp = False\n        else:\n            self.name = fn\n            self.close_fp = True\n            try:\n                self.f = open(fn, \"w+b\" if new else \"r+b\")\n            except OSError:\n                self.f = open(fn, \"w+b\")\n        self.beginning = self.f.tell()\n        self.setup()\n\n    def setup(self):\n        # Reset everything.\n        self.f.seek(self.beginning, os.SEEK_SET)\n\n        self.whereToWriteNewIFDOffset = None\n        self.offsetOfNewPage = 0\n\n        self.IIMM = iimm = self.f.read(4)\n        if not iimm:\n            # empty file - first page\n            self.isFirst = True\n            return\n\n        self.isFirst = False\n        if iimm == b\"II\\x2a\\x00\":\n            self.setEndian(\"<\")\n        elif iimm == b\"MM\\x00\\x2a\":\n            self.setEndian(\">\")\n        else:\n            raise RuntimeError(\"Invalid TIFF file header\")\n\n        self.skipIFDs()\n        self.goToEnd()\n\n    def finalize(self):\n        if self.isFirst:\n            return\n\n        # fix offsets\n        self.f.seek(self.offsetOfNewPage)\n\n        iimm = self.f.read(4)\n        if not iimm:\n            # raise RuntimeError(\"nothing written into new page\")\n            # Make it easy to finish a frame without committing to a new one.\n            return\n\n        if iimm != self.IIMM:\n            raise RuntimeError(\"IIMM of new page doesn't match IIMM of first page\")\n\n        ifd_offset = self.readLong()\n        ifd_offset += self.offsetOfNewPage\n        self.f.seek(self.whereToWriteNewIFDOffset)\n        self.writeLong(ifd_offset)\n        self.f.seek(ifd_offset)\n        self.fixIFD()\n\n    def newFrame(self):\n        # Call this to finish a frame.\n        self.finalize()\n        self.setup()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        if self.close_fp:\n            self.close()\n        return False\n\n    def tell(self):\n        return self.f.tell() - self.offsetOfNewPage\n\n    def seek(self, offset, whence=io.SEEK_SET):\n        if whence == os.SEEK_SET:\n            offset += self.offsetOfNewPage\n\n        self.f.seek(offset, whence)\n        return self.tell()\n\n    def goToEnd(self):\n        self.f.seek(0, os.SEEK_END)\n        pos = self.f.tell()\n\n        # pad to 16 byte boundary\n        pad_bytes = 16 - pos % 16\n        if 0 < pad_bytes < 16:\n            self.f.write(bytes(pad_bytes))\n        self.offsetOfNewPage = self.f.tell()\n\n    def setEndian(self, endian):\n        self.endian = endian\n        self.longFmt = self.endian + \"L\"\n        self.shortFmt = self.endian + \"H\"\n        self.tagFormat = self.endian + \"HHL\"\n\n    def skipIFDs(self):\n        while True:\n            ifd_offset = self.readLong()\n            if ifd_offset == 0:\n                self.whereToWriteNewIFDOffset = self.f.tell() - 4\n                break\n\n            self.f.seek(ifd_offset)\n            num_tags = self.readShort()\n            self.f.seek(num_tags * 12, os.SEEK_CUR)\n\n    def write(self, data):\n        return self.f.write(data)\n\n    def readShort(self):\n        (value,) = struct.unpack(self.shortFmt, self.f.read(2))\n        return value\n\n    def readLong(self):\n        (value,) = struct.unpack(self.longFmt, self.f.read(4))\n        return value\n\n    def rewriteLastShortToLong(self, value):\n        self.f.seek(-2, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def rewriteLastShort(self, value):\n        self.f.seek(-2, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.shortFmt, value))\n        if bytes_written is not None and bytes_written != 2:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 2\")\n\n    def rewriteLastLong(self, value):\n        self.f.seek(-4, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def writeShort(self, value):\n        bytes_written = self.f.write(struct.pack(self.shortFmt, value))\n        if bytes_written is not None and bytes_written != 2:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 2\")\n\n    def writeLong(self, value):\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def close(self):\n        self.finalize()\n        self.f.close()\n\n    def fixIFD(self):\n        num_tags = self.readShort()\n\n        for i in range(num_tags):\n            tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))\n\n            field_size = self.fieldSizes[field_type]\n            total_size = field_size * count\n            is_local = total_size <= 4\n            if not is_local:\n                offset = self.readLong()\n                offset += self.offsetOfNewPage\n                self.rewriteLastLong(offset)\n\n            if tag in self.Tags:\n                cur_pos = self.f.tell()\n\n                if is_local:\n                    self.fixOffsets(\n                        count, isShort=(field_size == 2), isLong=(field_size == 4)\n                    )\n                    self.f.seek(cur_pos + 4)\n                else:\n                    self.f.seek(offset)\n                    self.fixOffsets(\n                        count, isShort=(field_size == 2), isLong=(field_size == 4)\n                    )\n                    self.f.seek(cur_pos)\n\n                offset = cur_pos = None\n\n            elif is_local:\n                # skip the locally stored value that is not an offset\n                self.f.seek(4, os.SEEK_CUR)\n\n    def fixOffsets(self, count, isShort=False, isLong=False):\n        if not isShort and not isLong:\n            raise RuntimeError(\"offset is neither short nor long\")\n\n        for i in range(count):\n            offset = self.readShort() if isShort else self.readLong()\n            offset += self.offsetOfNewPage\n            if isShort and offset >= 65536:\n                # offset is now too large - we must convert shorts to longs\n                if count != 1:\n                    raise RuntimeError(\"not implemented\")  # XXX TODO\n\n                # simple case - the offset is just one and therefore it is\n                # local (not referenced with another offset)\n                self.rewriteLastShortToLong(offset)\n                self.f.seek(-10, os.SEEK_CUR)\n                self.writeShort(TiffTags.LONG)  # rewrite the type to LONG\n                self.f.seek(8, os.SEEK_CUR)\n            elif isShort:\n                self.rewriteLastShort(offset)\n            else:\n                self.rewriteLastLong(offset)\n\n\ndef _save_all(im, fp, filename):\n    encoderinfo = im.encoderinfo.copy()\n    encoderconfig = im.encoderconfig\n    append_images = list(encoderinfo.get(\"append_images\", []))\n    if not hasattr(im, \"n_frames\") and not append_images:\n        return _save(im, fp, filename)\n\n    cur_idx = im.tell()\n    try:\n        with AppendingTiffWriter(fp) as tf:\n            for ims in [im] + append_images:\n                ims.encoderinfo = encoderinfo\n                ims.encoderconfig = encoderconfig\n                if not hasattr(ims, \"n_frames\"):\n                    nfr = 1\n                else:\n                    nfr = ims.n_frames\n\n                for idx in range(nfr):\n                    ims.seek(idx)\n                    ims.load()\n                    _save(ims, tf, filename)\n                    tf.newFrame()\n    finally:\n        im.seek(cur_idx)\n\n\n#\n# --------------------------------------------------------------------\n# Register\n\nImage.register_open(TiffImageFile.format, TiffImageFile, _accept)\nImage.register_save(TiffImageFile.format, _save)\nImage.register_save_all(TiffImageFile.format, _save_all)\n\nImage.register_extensions(TiffImageFile.format, [\".tif\", \".tiff\"])\n\nImage.register_mime(TiffImageFile.format, \"image/tiff\")\n",
            "new_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# TIFF file handling\n#\n# TIFF is a flexible, if somewhat aged, image file format originally\n# defined by Aldus.  Although TIFF supports a wide variety of pixel\n# layouts and compression methods, the name doesn't really stand for\n# \"thousands of incompatible file formats,\" it just feels that way.\n#\n# To read TIFF data from a stream, the stream must be seekable.  For\n# progressive decoding, make sure to use TIFF files where the tag\n# directory is placed first in the file.\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-05-04 fl   Handle JPEGTABLES tag\n# 1996-05-18 fl   Fixed COLORMAP support\n# 1997-01-05 fl   Fixed PREDICTOR support\n# 1997-08-27 fl   Added support for rational tags (from Perry Stoll)\n# 1998-01-10 fl   Fixed seek/tell (from Jan Blom)\n# 1998-07-15 fl   Use private names for internal variables\n# 1999-06-13 fl   Rewritten for PIL 1.0 (1.0)\n# 2000-10-11 fl   Additional fixes for Python 2.0 (1.1)\n# 2001-04-17 fl   Fixed rewind support (seek to frame 0) (1.2)\n# 2001-05-12 fl   Added write support for more tags (from Greg Couch) (1.3)\n# 2001-12-18 fl   Added workaround for broken Matrox library\n# 2002-01-18 fl   Don't mess up if photometric tag is missing (D. Alan Stewart)\n# 2003-05-19 fl   Check FILLORDER tag\n# 2003-09-26 fl   Added RGBa support\n# 2004-02-24 fl   Added DPI support; fixed rational write support\n# 2005-02-07 fl   Added workaround for broken Corel Draw 10 files\n# 2006-01-09 fl   Added support for float/double tags (from Russell Nelson)\n#\n# Copyright (c) 1997-2006 by Secret Labs AB.  All rights reserved.\n# Copyright (c) 1995-1997 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\nimport io\nimport itertools\nimport logging\nimport math\nimport os\nimport struct\nimport warnings\nfrom collections.abc import MutableMapping\nfrom fractions import Fraction\nfrom numbers import Number, Rational\n\nfrom . import Image, ImageFile, ImageOps, ImagePalette, TiffTags\nfrom ._binary import i16be as i16\nfrom ._binary import i32be as i32\nfrom ._binary import o8\nfrom .TiffTags import TYPES\n\nlogger = logging.getLogger(__name__)\n\n# Set these to true to force use of libtiff for reading or writing.\nREAD_LIBTIFF = False\nWRITE_LIBTIFF = False\nIFD_LEGACY_API = True\nSTRIP_SIZE = 65536\n\nII = b\"II\"  # little-endian (Intel style)\nMM = b\"MM\"  # big-endian (Motorola style)\n\n#\n# --------------------------------------------------------------------\n# Read TIFF files\n\n# a few tag names, just to make the code below a bit more readable\nIMAGEWIDTH = 256\nIMAGELENGTH = 257\nBITSPERSAMPLE = 258\nCOMPRESSION = 259\nPHOTOMETRIC_INTERPRETATION = 262\nFILLORDER = 266\nIMAGEDESCRIPTION = 270\nSTRIPOFFSETS = 273\nSAMPLESPERPIXEL = 277\nROWSPERSTRIP = 278\nSTRIPBYTECOUNTS = 279\nX_RESOLUTION = 282\nY_RESOLUTION = 283\nPLANAR_CONFIGURATION = 284\nRESOLUTION_UNIT = 296\nTRANSFERFUNCTION = 301\nSOFTWARE = 305\nDATE_TIME = 306\nARTIST = 315\nPREDICTOR = 317\nCOLORMAP = 320\nTILEWIDTH = 322\nTILELENGTH = 323\nTILEOFFSETS = 324\nTILEBYTECOUNTS = 325\nSUBIFD = 330\nEXTRASAMPLES = 338\nSAMPLEFORMAT = 339\nJPEGTABLES = 347\nYCBCRSUBSAMPLING = 530\nREFERENCEBLACKWHITE = 532\nCOPYRIGHT = 33432\nIPTC_NAA_CHUNK = 33723  # newsphoto properties\nPHOTOSHOP_CHUNK = 34377  # photoshop properties\nICCPROFILE = 34675\nEXIFIFD = 34665\nXMP = 700\nJPEGQUALITY = 65537  # pseudo-tag by libtiff\n\n# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java\nIMAGEJ_META_DATA_BYTE_COUNTS = 50838\nIMAGEJ_META_DATA = 50839\n\nCOMPRESSION_INFO = {\n    # Compression => pil compression name\n    1: \"raw\",\n    2: \"tiff_ccitt\",\n    3: \"group3\",\n    4: \"group4\",\n    5: \"tiff_lzw\",\n    6: \"tiff_jpeg\",  # obsolete\n    7: \"jpeg\",\n    8: \"tiff_adobe_deflate\",\n    32771: \"tiff_raw_16\",  # 16-bit padding\n    32773: \"packbits\",\n    32809: \"tiff_thunderscan\",\n    32946: \"tiff_deflate\",\n    34676: \"tiff_sgilog\",\n    34677: \"tiff_sgilog24\",\n    34925: \"lzma\",\n    50000: \"zstd\",\n    50001: \"webp\",\n}\n\nCOMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}\n\nOPEN_INFO = {\n    # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,\n    #  ExtraSamples) => mode, rawmode\n    (II, 0, (1,), 1, (1,), ()): (\"1\", \"1;I\"),\n    (MM, 0, (1,), 1, (1,), ()): (\"1\", \"1;I\"),\n    (II, 0, (1,), 2, (1,), ()): (\"1\", \"1;IR\"),\n    (MM, 0, (1,), 2, (1,), ()): (\"1\", \"1;IR\"),\n    (II, 1, (1,), 1, (1,), ()): (\"1\", \"1\"),\n    (MM, 1, (1,), 1, (1,), ()): (\"1\", \"1\"),\n    (II, 1, (1,), 2, (1,), ()): (\"1\", \"1;R\"),\n    (MM, 1, (1,), 2, (1,), ()): (\"1\", \"1;R\"),\n    (II, 0, (1,), 1, (2,), ()): (\"L\", \"L;2I\"),\n    (MM, 0, (1,), 1, (2,), ()): (\"L\", \"L;2I\"),\n    (II, 0, (1,), 2, (2,), ()): (\"L\", \"L;2IR\"),\n    (MM, 0, (1,), 2, (2,), ()): (\"L\", \"L;2IR\"),\n    (II, 1, (1,), 1, (2,), ()): (\"L\", \"L;2\"),\n    (MM, 1, (1,), 1, (2,), ()): (\"L\", \"L;2\"),\n    (II, 1, (1,), 2, (2,), ()): (\"L\", \"L;2R\"),\n    (MM, 1, (1,), 2, (2,), ()): (\"L\", \"L;2R\"),\n    (II, 0, (1,), 1, (4,), ()): (\"L\", \"L;4I\"),\n    (MM, 0, (1,), 1, (4,), ()): (\"L\", \"L;4I\"),\n    (II, 0, (1,), 2, (4,), ()): (\"L\", \"L;4IR\"),\n    (MM, 0, (1,), 2, (4,), ()): (\"L\", \"L;4IR\"),\n    (II, 1, (1,), 1, (4,), ()): (\"L\", \"L;4\"),\n    (MM, 1, (1,), 1, (4,), ()): (\"L\", \"L;4\"),\n    (II, 1, (1,), 2, (4,), ()): (\"L\", \"L;4R\"),\n    (MM, 1, (1,), 2, (4,), ()): (\"L\", \"L;4R\"),\n    (II, 0, (1,), 1, (8,), ()): (\"L\", \"L;I\"),\n    (MM, 0, (1,), 1, (8,), ()): (\"L\", \"L;I\"),\n    (II, 0, (1,), 2, (8,), ()): (\"L\", \"L;IR\"),\n    (MM, 0, (1,), 2, (8,), ()): (\"L\", \"L;IR\"),\n    (II, 1, (1,), 1, (8,), ()): (\"L\", \"L\"),\n    (MM, 1, (1,), 1, (8,), ()): (\"L\", \"L\"),\n    (II, 1, (1,), 2, (8,), ()): (\"L\", \"L;R\"),\n    (MM, 1, (1,), 2, (8,), ()): (\"L\", \"L;R\"),\n    (II, 1, (1,), 1, (12,), ()): (\"I;16\", \"I;12\"),\n    (II, 0, (1,), 1, (16,), ()): (\"I;16\", \"I;16\"),\n    (II, 1, (1,), 1, (16,), ()): (\"I;16\", \"I;16\"),\n    (MM, 1, (1,), 1, (16,), ()): (\"I;16B\", \"I;16B\"),\n    (II, 1, (1,), 2, (16,), ()): (\"I;16\", \"I;16R\"),\n    (II, 1, (2,), 1, (16,), ()): (\"I\", \"I;16S\"),\n    (MM, 1, (2,), 1, (16,), ()): (\"I\", \"I;16BS\"),\n    (II, 0, (3,), 1, (32,), ()): (\"F\", \"F;32F\"),\n    (MM, 0, (3,), 1, (32,), ()): (\"F\", \"F;32BF\"),\n    (II, 1, (1,), 1, (32,), ()): (\"I\", \"I;32N\"),\n    (II, 1, (2,), 1, (32,), ()): (\"I\", \"I;32S\"),\n    (MM, 1, (2,), 1, (32,), ()): (\"I\", \"I;32BS\"),\n    (II, 1, (3,), 1, (32,), ()): (\"F\", \"F;32F\"),\n    (MM, 1, (3,), 1, (32,), ()): (\"F\", \"F;32BF\"),\n    (II, 1, (1,), 1, (8, 8), (2,)): (\"LA\", \"LA\"),\n    (MM, 1, (1,), 1, (8, 8), (2,)): (\"LA\", \"LA\"),\n    (II, 2, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGB\"),\n    (MM, 2, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGB\"),\n    (II, 2, (1,), 2, (8, 8, 8), ()): (\"RGB\", \"RGB;R\"),\n    (MM, 2, (1,), 2, (8, 8, 8), ()): (\"RGB\", \"RGB;R\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), ()): (\"RGBA\", \"RGBA\"),  # missing ExtraSamples\n    (MM, 2, (1,), 1, (8, 8, 8, 8), ()): (\"RGBA\", \"RGBA\"),  # missing ExtraSamples\n    (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): (\"RGBX\", \"RGBX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): (\"RGBX\", \"RGBX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): (\"RGBX\", \"RGBXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): (\"RGBX\", \"RGBXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): (\"RGBX\", \"RGBXXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): (\"RGBX\", \"RGBXXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): (\"RGBA\", \"RGBa\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): (\"RGBA\", \"RGBa\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): (\"RGBA\", \"RGBaX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): (\"RGBA\", \"RGBaX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): (\"RGBA\", \"RGBaXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): (\"RGBA\", \"RGBaXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): (\"RGBA\", \"RGBA\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): (\"RGBA\", \"RGBA\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): (\"RGBA\", \"RGBAX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): (\"RGBA\", \"RGBAX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): (\"RGBA\", \"RGBAXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): (\"RGBA\", \"RGBAXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): (\"RGBA\", \"RGBA\"),  # Corel Draw 10\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): (\"RGBA\", \"RGBA\"),  # Corel Draw 10\n    (II, 2, (1,), 1, (16, 16, 16), ()): (\"RGB\", \"RGB;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16), ()): (\"RGB\", \"RGB;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), ()): (\"RGBA\", \"RGBA;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), ()): (\"RGBA\", \"RGBA;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): (\"RGBX\", \"RGBX;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): (\"RGBX\", \"RGBX;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): (\"RGBA\", \"RGBa;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): (\"RGBA\", \"RGBa;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): (\"RGBA\", \"RGBA;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): (\"RGBA\", \"RGBA;16B\"),\n    (II, 3, (1,), 1, (1,), ()): (\"P\", \"P;1\"),\n    (MM, 3, (1,), 1, (1,), ()): (\"P\", \"P;1\"),\n    (II, 3, (1,), 2, (1,), ()): (\"P\", \"P;1R\"),\n    (MM, 3, (1,), 2, (1,), ()): (\"P\", \"P;1R\"),\n    (II, 3, (1,), 1, (2,), ()): (\"P\", \"P;2\"),\n    (MM, 3, (1,), 1, (2,), ()): (\"P\", \"P;2\"),\n    (II, 3, (1,), 2, (2,), ()): (\"P\", \"P;2R\"),\n    (MM, 3, (1,), 2, (2,), ()): (\"P\", \"P;2R\"),\n    (II, 3, (1,), 1, (4,), ()): (\"P\", \"P;4\"),\n    (MM, 3, (1,), 1, (4,), ()): (\"P\", \"P;4\"),\n    (II, 3, (1,), 2, (4,), ()): (\"P\", \"P;4R\"),\n    (MM, 3, (1,), 2, (4,), ()): (\"P\", \"P;4R\"),\n    (II, 3, (1,), 1, (8,), ()): (\"P\", \"P\"),\n    (MM, 3, (1,), 1, (8,), ()): (\"P\", \"P\"),\n    (II, 3, (1,), 1, (8, 8), (2,)): (\"PA\", \"PA\"),\n    (MM, 3, (1,), 1, (8, 8), (2,)): (\"PA\", \"PA\"),\n    (II, 3, (1,), 2, (8,), ()): (\"P\", \"P;R\"),\n    (MM, 3, (1,), 2, (8,), ()): (\"P\", \"P;R\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8), ()): (\"CMYK\", \"CMYK\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8), ()): (\"CMYK\", \"CMYK\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): (\"CMYK\", \"CMYKX\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): (\"CMYK\", \"CMYKX\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): (\"CMYK\", \"CMYKXX\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): (\"CMYK\", \"CMYKXX\"),\n    (II, 5, (1,), 1, (16, 16, 16, 16), ()): (\"CMYK\", \"CMYK;16L\"),\n    # JPEG compressed images handled by LibTiff and auto-converted to RGBX\n    # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel\n    (II, 6, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGBX\"),\n    (MM, 6, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGBX\"),\n    (II, 8, (1,), 1, (8, 8, 8), ()): (\"LAB\", \"LAB\"),\n    (MM, 8, (1,), 1, (8, 8, 8), ()): (\"LAB\", \"LAB\"),\n}\n\nMAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())\n\nPREFIXES = [\n    b\"MM\\x00\\x2A\",  # Valid TIFF header with big-endian byte order\n    b\"II\\x2A\\x00\",  # Valid TIFF header with little-endian byte order\n    b\"MM\\x2A\\x00\",  # Invalid TIFF header, assume big-endian\n    b\"II\\x00\\x2A\",  # Invalid TIFF header, assume little-endian\n    b\"MM\\x00\\x2B\",  # BigTIFF with big-endian byte order\n    b\"II\\x2B\\x00\",  # BigTIFF with little-endian byte order\n]\n\n\ndef _accept(prefix):\n    return prefix[:4] in PREFIXES\n\n\ndef _limit_rational(val, max_val):\n    inv = abs(val) > 1\n    n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)\n    return n_d[::-1] if inv else n_d\n\n\ndef _limit_signed_rational(val, max_val, min_val):\n    frac = Fraction(val)\n    n_d = frac.numerator, frac.denominator\n\n    if min(n_d) < min_val:\n        n_d = _limit_rational(val, abs(min_val))\n\n    if max(n_d) > max_val:\n        val = Fraction(*n_d)\n        n_d = _limit_rational(val, max_val)\n\n    return n_d\n\n\n##\n# Wrapper for TIFF IFDs.\n\n_load_dispatch = {}\n_write_dispatch = {}\n\n\nclass IFDRational(Rational):\n    \"\"\"Implements a rational class where 0/0 is a legal value to match\n    the in the wild use of exif rationals.\n\n    e.g., DigitalZoomRatio - 0.00/0.00  indicates that no digital zoom was used\n    \"\"\"\n\n    \"\"\" If the denominator is 0, store this as a float('nan'), otherwise store\n    as a fractions.Fraction(). Delegate as appropriate\n\n    \"\"\"\n\n    __slots__ = (\"_numerator\", \"_denominator\", \"_val\")\n\n    def __init__(self, value, denominator=1):\n        \"\"\"\n        :param value: either an integer numerator, a\n        float/rational/other number, or an IFDRational\n        :param denominator: Optional integer denominator\n        \"\"\"\n        if isinstance(value, IFDRational):\n            self._numerator = value.numerator\n            self._denominator = value.denominator\n            self._val = value._val\n            return\n\n        if isinstance(value, Fraction):\n            self._numerator = value.numerator\n            self._denominator = value.denominator\n        else:\n            self._numerator = value\n            self._denominator = denominator\n\n        if denominator == 0:\n            self._val = float(\"nan\")\n        elif denominator == 1:\n            self._val = Fraction(value)\n        else:\n            self._val = Fraction(value, denominator)\n\n    @property\n    def numerator(self):\n        return self._numerator\n\n    @property\n    def denominator(self):\n        return self._denominator\n\n    def limit_rational(self, max_denominator):\n        \"\"\"\n\n        :param max_denominator: Integer, the maximum denominator value\n        :returns: Tuple of (numerator, denominator)\n        \"\"\"\n\n        if self.denominator == 0:\n            return self.numerator, self.denominator\n\n        f = self._val.limit_denominator(max_denominator)\n        return f.numerator, f.denominator\n\n    def __repr__(self):\n        return str(float(self._val))\n\n    def __hash__(self):\n        return self._val.__hash__()\n\n    def __eq__(self, other):\n        val = self._val\n        if isinstance(other, IFDRational):\n            other = other._val\n        if isinstance(other, float):\n            val = float(val)\n        return val == other\n\n    def __getstate__(self):\n        return [self._val, self._numerator, self._denominator]\n\n    def __setstate__(self, state):\n        IFDRational.__init__(self, 0)\n        _val, _numerator, _denominator = state\n        self._val = _val\n        self._numerator = _numerator\n        self._denominator = _denominator\n\n    def _delegate(op):\n        def delegate(self, *args):\n            return getattr(self._val, op)(*args)\n\n        return delegate\n\n    \"\"\" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',\n             'truediv', 'rtruediv', 'floordiv', 'rfloordiv',\n             'mod','rmod', 'pow','rpow', 'pos', 'neg',\n             'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',\n             'ceil', 'floor', 'round']\n        print(\"\\n\".join(\"__%s__ = _delegate('__%s__')\" % (s,s) for s in a))\n        \"\"\"\n\n    __add__ = _delegate(\"__add__\")\n    __radd__ = _delegate(\"__radd__\")\n    __sub__ = _delegate(\"__sub__\")\n    __rsub__ = _delegate(\"__rsub__\")\n    __mul__ = _delegate(\"__mul__\")\n    __rmul__ = _delegate(\"__rmul__\")\n    __truediv__ = _delegate(\"__truediv__\")\n    __rtruediv__ = _delegate(\"__rtruediv__\")\n    __floordiv__ = _delegate(\"__floordiv__\")\n    __rfloordiv__ = _delegate(\"__rfloordiv__\")\n    __mod__ = _delegate(\"__mod__\")\n    __rmod__ = _delegate(\"__rmod__\")\n    __pow__ = _delegate(\"__pow__\")\n    __rpow__ = _delegate(\"__rpow__\")\n    __pos__ = _delegate(\"__pos__\")\n    __neg__ = _delegate(\"__neg__\")\n    __abs__ = _delegate(\"__abs__\")\n    __trunc__ = _delegate(\"__trunc__\")\n    __lt__ = _delegate(\"__lt__\")\n    __gt__ = _delegate(\"__gt__\")\n    __le__ = _delegate(\"__le__\")\n    __ge__ = _delegate(\"__ge__\")\n    __bool__ = _delegate(\"__bool__\")\n    __ceil__ = _delegate(\"__ceil__\")\n    __floor__ = _delegate(\"__floor__\")\n    __round__ = _delegate(\"__round__\")\n\n\nclass ImageFileDirectory_v2(MutableMapping):\n    \"\"\"This class represents a TIFF tag directory.  To speed things up, we\n    don't decode tags unless they're asked for.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v2()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        'Some Data'\n\n    Individual values are returned as the strings or numbers, sequences are\n    returned as tuples of the values.\n\n    The tiff metadata type of each item is stored in a dictionary of\n    tag types in\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types\n    are read from a tiff file, guessed from the type added, or added\n    manually.\n\n    Data Structures:\n\n        * ``self.tagtype = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: integer corresponding to the data type from\n            :py:data:`.TiffTags.TYPES`\n\n          .. versionadded:: 3.0.0\n\n    'Internal' data structures:\n\n        * ``self._tags_v2 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data, as tuple for multiple values\n\n        * ``self._tagdata = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: undecoded byte string from file\n\n        * ``self._tags_v1 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data in the v1 format\n\n    Tags will be found in the private attributes ``self._tagdata``, and in\n    ``self._tags_v2`` once decoded.\n\n    ``self.legacy_api`` is a value for internal use, and shouldn't be changed\n    from outside code. In cooperation with\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``\n    is true, then decoded tags will be populated into both ``_tags_v1`` and\n    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF\n    save routine. Tags should be read from ``_tags_v1`` if\n    ``legacy_api == true``.\n\n    \"\"\"\n\n    def __init__(self, ifh=b\"II\\052\\0\\0\\0\\0\\0\", prefix=None, group=None):\n        \"\"\"Initialize an ImageFileDirectory.\n\n        To construct an ImageFileDirectory from a real file, pass the 8-byte\n        magic header to the constructor.  To only set the endianness, pass it\n        as the 'prefix' keyword argument.\n\n        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets\n              endianness.\n        :param prefix: Override the endianness of the file.\n        \"\"\"\n        if not _accept(ifh):\n            raise SyntaxError(f\"not a TIFF file (header {repr(ifh)} not valid)\")\n        self._prefix = prefix if prefix is not None else ifh[:2]\n        if self._prefix == MM:\n            self._endian = \">\"\n        elif self._prefix == II:\n            self._endian = \"<\"\n        else:\n            raise SyntaxError(\"not a TIFF IFD\")\n        self._bigtiff = ifh[2] == 43\n        self.group = group\n        self.tagtype = {}\n        \"\"\" Dictionary of tag types \"\"\"\n        self.reset()\n        (self.next,) = (\n            self._unpack(\"Q\", ifh[8:]) if self._bigtiff else self._unpack(\"L\", ifh[4:])\n        )\n        self._legacy_api = False\n\n    prefix = property(lambda self: self._prefix)\n    offset = property(lambda self: self._offset)\n    legacy_api = property(lambda self: self._legacy_api)\n\n    @legacy_api.setter\n    def legacy_api(self, value):\n        raise Exception(\"Not allowing setting of legacy api\")\n\n    def reset(self):\n        self._tags_v1 = {}  # will remain empty if legacy_api is false\n        self._tags_v2 = {}  # main tag storage\n        self._tagdata = {}\n        self.tagtype = {}  # added 2008-06-05 by Florian Hoech\n        self._next = None\n        self._offset = None\n\n    def __str__(self):\n        return str(dict(self))\n\n    def named(self):\n        \"\"\"\n        :returns: dict of name|key: value\n\n        Returns the complete tag dictionary, with named tags where possible.\n        \"\"\"\n        return {\n            TiffTags.lookup(code, self.group).name: value\n            for code, value in self.items()\n        }\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v2))\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v2:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            self[tag] = handler(self, data, self.legacy_api)  # check type\n        val = self._tags_v2[tag]\n        if self.legacy_api and not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n    def __contains__(self, tag):\n        return tag in self._tags_v2 or tag in self._tagdata\n\n    def __setitem__(self, tag, value):\n        self._setitem(tag, value, self.legacy_api)\n\n    def _setitem(self, tag, value, legacy_api):\n        basetypes = (Number, bytes, str)\n\n        info = TiffTags.lookup(tag, self.group)\n        values = [value] if isinstance(value, basetypes) else value\n\n        if tag not in self.tagtype:\n            if info.type:\n                self.tagtype[tag] = info.type\n            else:\n                self.tagtype[tag] = TiffTags.UNDEFINED\n                if all(isinstance(v, IFDRational) for v in values):\n                    self.tagtype[tag] = (\n                        TiffTags.RATIONAL\n                        if all(v >= 0 for v in values)\n                        else TiffTags.SIGNED_RATIONAL\n                    )\n                elif all(isinstance(v, int) for v in values):\n                    if all(0 <= v < 2**16 for v in values):\n                        self.tagtype[tag] = TiffTags.SHORT\n                    elif all(-(2**15) < v < 2**15 for v in values):\n                        self.tagtype[tag] = TiffTags.SIGNED_SHORT\n                    else:\n                        self.tagtype[tag] = (\n                            TiffTags.LONG\n                            if all(v >= 0 for v in values)\n                            else TiffTags.SIGNED_LONG\n                        )\n                elif all(isinstance(v, float) for v in values):\n                    self.tagtype[tag] = TiffTags.DOUBLE\n                elif all(isinstance(v, str) for v in values):\n                    self.tagtype[tag] = TiffTags.ASCII\n                elif all(isinstance(v, bytes) for v in values):\n                    self.tagtype[tag] = TiffTags.BYTE\n\n        if self.tagtype[tag] == TiffTags.UNDEFINED:\n            values = [\n                v.encode(\"ascii\", \"replace\") if isinstance(v, str) else v\n                for v in values\n            ]\n        elif self.tagtype[tag] == TiffTags.RATIONAL:\n            values = [float(v) if isinstance(v, int) else v for v in values]\n\n        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)\n        if not is_ifd:\n            values = tuple(info.cvt_enum(value) for value in values)\n\n        dest = self._tags_v1 if legacy_api else self._tags_v2\n\n        # Three branches:\n        # Spec'd length == 1, Actual length 1, store as element\n        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.\n        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.\n        # Don't mess with the legacy api, since it's frozen.\n        if not is_ifd and (\n            (info.length == 1)\n            or self.tagtype[tag] == TiffTags.BYTE\n            or (info.length is None and len(values) == 1 and not legacy_api)\n        ):\n            # Don't mess with the legacy api, since it's frozen.\n            if legacy_api and self.tagtype[tag] in [\n                TiffTags.RATIONAL,\n                TiffTags.SIGNED_RATIONAL,\n            ]:  # rationals\n                values = (values,)\n            try:\n                (dest[tag],) = values\n            except ValueError:\n                # We've got a builtin tag with 1 expected entry\n                warnings.warn(\n                    f\"Metadata Warning, tag {tag} had too many entries: \"\n                    f\"{len(values)}, expected 1\"\n                )\n                dest[tag] = values[0]\n\n        else:\n            # Spec'd length > 1 or undefined\n            # Unspec'd, and length > 1\n            dest[tag] = values\n\n    def __delitem__(self, tag):\n        self._tags_v2.pop(tag, None)\n        self._tags_v1.pop(tag, None)\n        self._tagdata.pop(tag, None)\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v2))\n\n    def _unpack(self, fmt, data):\n        return struct.unpack(self._endian + fmt, data)\n\n    def _pack(self, fmt, *values):\n        return struct.pack(self._endian + fmt, *values)\n\n    def _register_loader(idx, size):\n        def decorator(func):\n            from .TiffTags import TYPES\n\n            if func.__name__.startswith(\"load_\"):\n                TYPES[idx] = func.__name__[5:].replace(\"_\", \" \")\n            _load_dispatch[idx] = size, func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_writer(idx):\n        def decorator(func):\n            _write_dispatch[idx] = func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_basic(idx_fmt_name):\n        from .TiffTags import TYPES\n\n        idx, fmt, name = idx_fmt_name\n        TYPES[idx] = name\n        size = struct.calcsize(\"=\" + fmt)\n        _load_dispatch[idx] = (  # noqa: F821\n            size,\n            lambda self, data, legacy_api=True: (\n                self._unpack(f\"{len(data) // size}{fmt}\", data)\n            ),\n        )\n        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821\n            b\"\".join(self._pack(fmt, value) for value in values)\n        )\n\n    list(\n        map(\n            _register_basic,\n            [\n                (TiffTags.SHORT, \"H\", \"short\"),\n                (TiffTags.LONG, \"L\", \"long\"),\n                (TiffTags.SIGNED_BYTE, \"b\", \"signed byte\"),\n                (TiffTags.SIGNED_SHORT, \"h\", \"signed short\"),\n                (TiffTags.SIGNED_LONG, \"l\", \"signed long\"),\n                (TiffTags.FLOAT, \"f\", \"float\"),\n                (TiffTags.DOUBLE, \"d\", \"double\"),\n                (TiffTags.IFD, \"L\", \"long\"),\n                (TiffTags.LONG8, \"Q\", \"long8\"),\n            ],\n        )\n    )\n\n    @_register_loader(1, 1)  # Basic type, except for the legacy API.\n    def load_byte(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(1)  # Basic type, except for the legacy API.\n    def write_byte(self, data):\n        if isinstance(data, int):\n            data = bytes((data,))\n        return data\n\n    @_register_loader(2, 1)\n    def load_string(self, data, legacy_api=True):\n        if data.endswith(b\"\\0\"):\n            data = data[:-1]\n        return data.decode(\"latin-1\", \"replace\")\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if isinstance(value, int):\n            value = str(value)\n        if not isinstance(value, bytes):\n            value = value.encode(\"ascii\", \"replace\")\n        return value + b\"\\0\"\n\n    @_register_loader(5, 8)\n    def load_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}L\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(5)\n    def write_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2L\", *_limit_rational(frac, 2**32 - 1)) for frac in values\n        )\n\n    @_register_loader(7, 1)\n    def load_undefined(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(7)\n    def write_undefined(self, value):\n        return value\n\n    @_register_loader(10, 8)\n    def load_signed_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}l\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(10)\n    def write_signed_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2l\", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))\n            for frac in values\n        )\n\n    def _ensure_read(self, fp, size):\n        ret = fp.read(size)\n        if len(ret) != size:\n            raise OSError(\n                \"Corrupt EXIF data.  \"\n                f\"Expecting to read {size} bytes but only got {len(ret)}. \"\n            )\n        return ret\n\n    def load(self, fp):\n\n        self.reset()\n        self._offset = fp.tell()\n\n        try:\n            tag_count = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"H\", self._ensure_read(fp, 2))\n            )[0]\n            for i in range(tag_count):\n                tag, typ, count, data = (\n                    self._unpack(\"HHQ8s\", self._ensure_read(fp, 20))\n                    if self._bigtiff\n                    else self._unpack(\"HHL4s\", self._ensure_read(fp, 12))\n                )\n\n                tagname = TiffTags.lookup(tag, self.group).name\n                typname = TYPES.get(typ, \"unknown\")\n                msg = f\"tag: {tagname} ({tag}) - type: {typname} ({typ})\"\n\n                try:\n                    unit_size, handler = self._load_dispatch[typ]\n                except KeyError:\n                    logger.debug(msg + f\" - unsupported type {typ}\")\n                    continue  # ignore unsupported type\n                size = count * unit_size\n                if size > (8 if self._bigtiff else 4):\n                    here = fp.tell()\n                    (offset,) = self._unpack(\"Q\" if self._bigtiff else \"L\", data)\n                    msg += f\" Tag Location: {here} - Data Location: {offset}\"\n                    fp.seek(offset)\n                    data = ImageFile._safe_read(fp, size)\n                    fp.seek(here)\n                else:\n                    data = data[:size]\n\n                if len(data) != size:\n                    warnings.warn(\n                        \"Possibly corrupt EXIF data.  \"\n                        f\"Expecting to read {size} bytes but only got {len(data)}.\"\n                        f\" Skipping tag {tag}\"\n                    )\n                    logger.debug(msg)\n                    continue\n\n                if not data:\n                    logger.debug(msg)\n                    continue\n\n                self._tagdata[tag] = data\n                self.tagtype[tag] = typ\n\n                msg += \" - value: \" + (\n                    \"<table: %d bytes>\" % size if size > 32 else repr(data)\n                )\n                logger.debug(msg)\n\n            (self.next,) = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"L\", self._ensure_read(fp, 4))\n            )\n        except OSError as msg:\n            warnings.warn(str(msg))\n            return\n\n    def tobytes(self, offset=0):\n        # FIXME What about tagdata?\n        result = self._pack(\"H\", len(self._tags_v2))\n\n        entries = []\n        offset = offset + len(result) + len(self._tags_v2) * 12 + 4\n        stripoffsets = None\n\n        # pass 1: convert tags to binary format\n        # always write tags in ascending order\n        for tag, value in sorted(self._tags_v2.items()):\n            if tag == STRIPOFFSETS:\n                stripoffsets = len(entries)\n            typ = self.tagtype.get(tag)\n            logger.debug(f\"Tag {tag}, Type: {typ}, Value: {repr(value)}\")\n            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)\n            if is_ifd:\n                if self._endian == \"<\":\n                    ifh = b\"II\\x2A\\x00\\x08\\x00\\x00\\x00\"\n                else:\n                    ifh = b\"MM\\x00\\x2A\\x00\\x00\\x00\\x08\"\n                ifd = ImageFileDirectory_v2(ifh, group=tag)\n                values = self._tags_v2[tag]\n                for ifd_tag, ifd_value in values.items():\n                    ifd[ifd_tag] = ifd_value\n                data = ifd.tobytes(offset)\n            else:\n                values = value if isinstance(value, tuple) else (value,)\n                data = self._write_dispatch[typ](self, *values)\n\n            tagname = TiffTags.lookup(tag, self.group).name\n            typname = \"ifd\" if is_ifd else TYPES.get(typ, \"unknown\")\n            msg = f\"save: {tagname} ({tag}) - type: {typname} ({typ})\"\n            msg += \" - value: \" + (\n                \"<table: %d bytes>\" % len(data) if len(data) >= 16 else str(values)\n            )\n            logger.debug(msg)\n\n            # count is sum of lengths for string and arbitrary data\n            if is_ifd:\n                count = 1\n            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:\n                count = len(data)\n            else:\n                count = len(values)\n            # figure out if data fits into the entry\n            if len(data) <= 4:\n                entries.append((tag, typ, count, data.ljust(4, b\"\\0\"), b\"\"))\n            else:\n                entries.append((tag, typ, count, self._pack(\"L\", offset), data))\n                offset += (len(data) + 1) // 2 * 2  # pad to word\n\n        # update strip offset data to point beyond auxiliary data\n        if stripoffsets is not None:\n            tag, typ, count, value, data = entries[stripoffsets]\n            if data:\n                raise NotImplementedError(\"multistrip support not yet implemented\")\n            value = self._pack(\"L\", self._unpack(\"L\", value)[0] + offset)\n            entries[stripoffsets] = tag, typ, count, value, data\n\n        # pass 2: write entries to file\n        for tag, typ, count, value, data in entries:\n            logger.debug(f\"{tag} {typ} {count} {repr(value)} {repr(data)}\")\n            result += self._pack(\"HHL4s\", tag, typ, count, value)\n\n        # -- overwrite here for multi-page --\n        result += b\"\\0\\0\\0\\0\"  # end of entries\n\n        # pass 3: write auxiliary data to file\n        for tag, typ, count, value, data in entries:\n            result += data\n            if len(data) & 1:\n                result += b\"\\0\"\n\n        return result\n\n    def save(self, fp):\n\n        if fp.tell() == 0:  # skip TIFF header on subsequent pages\n            # tiff header -- PIL always starts the first IFD at offset 8\n            fp.write(self._prefix + self._pack(\"HL\", 42, 8))\n\n        offset = fp.tell()\n        result = self.tobytes(offset)\n        fp.write(result)\n        return offset + len(result)\n\n\nImageFileDirectory_v2._load_dispatch = _load_dispatch\nImageFileDirectory_v2._write_dispatch = _write_dispatch\nfor idx, name in TYPES.items():\n    name = name.replace(\" \", \"_\")\n    setattr(ImageFileDirectory_v2, \"load_\" + name, _load_dispatch[idx][1])\n    setattr(ImageFileDirectory_v2, \"write_\" + name, _write_dispatch[idx])\ndel _load_dispatch, _write_dispatch, idx, name\n\n\n# Legacy ImageFileDirectory support.\nclass ImageFileDirectory_v1(ImageFileDirectory_v2):\n    \"\"\"This class represents the **legacy** interface to a TIFF tag directory.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v1()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        ('Some Data',)\n\n    Also contains a dictionary of tag types as read from the tiff image file,\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.\n\n    Values are returned as a tuple.\n\n    ..  deprecated:: 3.0.0\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._legacy_api = True\n\n    tags = property(lambda self: self._tags_v1)\n    tagdata = property(lambda self: self._tagdata)\n\n    # defined in ImageFileDirectory_v2\n    tagtype: dict\n    \"\"\"Dictionary of tag types\"\"\"\n\n    @classmethod\n    def from_v2(cls, original):\n        \"\"\"Returns an\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n        instance with the same data as is contained in the original\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n        instance.\n\n        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n\n        \"\"\"\n\n        ifd = cls(prefix=original.prefix)\n        ifd._tagdata = original._tagdata\n        ifd.tagtype = original.tagtype\n        ifd.next = original.next  # an indicator for multipage tiffs\n        return ifd\n\n    def to_v2(self):\n        \"\"\"Returns an\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n        instance with the same data as is contained in the original\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n        instance.\n\n        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n\n        \"\"\"\n\n        ifd = ImageFileDirectory_v2(prefix=self.prefix)\n        ifd._tagdata = dict(self._tagdata)\n        ifd.tagtype = dict(self.tagtype)\n        ifd._tags_v2 = dict(self._tags_v2)\n        return ifd\n\n    def __contains__(self, tag):\n        return tag in self._tags_v1 or tag in self._tagdata\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v1))\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v1))\n\n    def __setitem__(self, tag, value):\n        for legacy_api in (False, True):\n            self._setitem(tag, value, legacy_api)\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v1:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            for legacy in (False, True):\n                self._setitem(tag, handler(self, data, legacy), legacy)\n        val = self._tags_v1[tag]\n        if not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n\n# undone -- switch this pointer when IFD_LEGACY_API == False\nImageFileDirectory = ImageFileDirectory_v1\n\n\n##\n# Image plugin for TIFF files.\n\n\nclass TiffImageFile(ImageFile.ImageFile):\n\n    format = \"TIFF\"\n    format_description = \"Adobe TIFF\"\n    _close_exclusive_fp_after_loading = False\n\n    def __init__(self, fp=None, filename=None):\n        self.tag_v2 = None\n        \"\"\" Image file directory (tag dictionary) \"\"\"\n\n        self.tag = None\n        \"\"\" Legacy tag entries \"\"\"\n\n        super().__init__(fp, filename)\n\n    def _open(self):\n        \"\"\"Open the first image in a TIFF file\"\"\"\n\n        # Header\n        ifh = self.fp.read(8)\n        if ifh[2] == 43:\n            ifh += self.fp.read(8)\n\n        self.tag_v2 = ImageFileDirectory_v2(ifh)\n\n        # legacy IFD entries will be filled in later\n        self.ifd = None\n\n        # setup frame pointers\n        self.__first = self.__next = self.tag_v2.next\n        self.__frame = -1\n        self._fp = self.fp\n        self._frame_pos = []\n        self._n_frames = None\n\n        logger.debug(\"*** TiffImageFile._open ***\")\n        logger.debug(f\"- __first: {self.__first}\")\n        logger.debug(f\"- ifh: {repr(ifh)}\")  # Use repr to avoid str(bytes)\n\n        # and load the first frame\n        self._seek(0)\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            self._seek(len(self._frame_pos))\n            while self._n_frames is None:\n                self._seek(self.tell() + 1)\n            self.seek(current)\n        return self._n_frames\n\n    def seek(self, frame):\n        \"\"\"Select a given frame as current image\"\"\"\n        if not self._seek_check(frame):\n            return\n        self._seek(frame)\n        # Create a new core image object on second and\n        # subsequent frames in the image. Image may be\n        # different size/mode.\n        Image._decompression_bomb_check(self.size)\n        self.im = Image.core.new(self.mode, self.size)\n\n    def _seek(self, frame):\n        self.fp = self._fp\n\n        # reset buffered io handle in case fp\n        # was passed to libtiff, invalidating the buffer\n        self.fp.tell()\n\n        while len(self._frame_pos) <= frame:\n            if not self.__next:\n                raise EOFError(\"no more images in TIFF file\")\n            logger.debug(\n                f\"Seeking to frame {frame}, on frame {self.__frame}, \"\n                f\"__next {self.__next}, location: {self.fp.tell()}\"\n            )\n            self.fp.seek(self.__next)\n            self._frame_pos.append(self.__next)\n            logger.debug(\"Loading tags, location: %s\" % self.fp.tell())\n            self.tag_v2.load(self.fp)\n            if self.tag_v2.next in self._frame_pos:\n                # This IFD has already been processed\n                # Declare this to be the end of the image\n                self.__next = 0\n            else:\n                self.__next = self.tag_v2.next\n            if self.__next == 0:\n                self._n_frames = frame + 1\n            if len(self._frame_pos) == 1:\n                self.is_animated = self.__next != 0\n            self.__frame += 1\n        self.fp.seek(self._frame_pos[frame])\n        self.tag_v2.load(self.fp)\n        self._reload_exif()\n        # fill the legacy tag/ifd entries\n        self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)\n        self.__frame = frame\n        self._setup()\n\n    def tell(self):\n        \"\"\"Return the current frame number\"\"\"\n        return self.__frame\n\n    def get_child_images(self):\n        if SUBIFD not in self.tag_v2:\n            return []\n        child_images = []\n        exif = self.getexif()\n        offset = None\n        for im_offset in self.tag_v2[SUBIFD]:\n            # reset buffered io handle in case fp\n            # was passed to libtiff, invalidating the buffer\n            current_offset = self._fp.tell()\n            if offset is None:\n                offset = current_offset\n\n            fp = self._fp\n            ifd = exif._get_ifd_dict(im_offset)\n            jpegInterchangeFormat = ifd.get(513)\n            if jpegInterchangeFormat is not None:\n                fp.seek(jpegInterchangeFormat)\n                jpeg_data = fp.read(ifd.get(514))\n\n                fp = io.BytesIO(jpeg_data)\n\n            with Image.open(fp) as im:\n                if jpegInterchangeFormat is None:\n                    im._frame_pos = [im_offset]\n                    im._seek(0)\n                im.load()\n                child_images.append(im)\n\n        if offset is not None:\n            self._fp.seek(offset)\n        return child_images\n\n    def getxmp(self):\n        \"\"\"\n        Returns a dictionary containing the XMP tags.\n        Requires defusedxml to be installed.\n\n        :returns: XMP tags in a dictionary.\n        \"\"\"\n        return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}\n\n    def get_photoshop_blocks(self):\n        \"\"\"\n        Returns a dictionary of Photoshop \"Image Resource Blocks\".\n        The keys are the image resource ID. For more information, see\n        https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727\n\n        :returns: Photoshop \"Image Resource Blocks\" in a dictionary.\n        \"\"\"\n        blocks = {}\n        val = self.tag_v2.get(0x8649)\n        if val:\n            while val[:4] == b\"8BIM\":\n                id = i16(val[4:6])\n                n = math.ceil((val[6] + 1) / 2) * 2\n                size = i32(val[6 + n : 10 + n])\n                data = val[10 + n : 10 + n + size]\n                blocks[id] = {\"data\": data}\n\n                val = val[math.ceil((10 + n + size) / 2) * 2 :]\n        return blocks\n\n    def load(self):\n        if self.tile and self.use_load_libtiff:\n            return self._load_libtiff()\n        return super().load()\n\n    def load_end(self):\n        if self._tile_orientation:\n            method = {\n                2: Image.Transpose.FLIP_LEFT_RIGHT,\n                3: Image.Transpose.ROTATE_180,\n                4: Image.Transpose.FLIP_TOP_BOTTOM,\n                5: Image.Transpose.TRANSPOSE,\n                6: Image.Transpose.ROTATE_270,\n                7: Image.Transpose.TRANSVERSE,\n                8: Image.Transpose.ROTATE_90,\n            }.get(self._tile_orientation)\n            if method is not None:\n                self.im = self.im.transpose(method)\n                self._size = self.im.size\n\n        # allow closing if we're on the first frame, there's no next\n        # This is the ImageFile.load path only, libtiff specific below.\n        if not self.is_animated:\n            self._close_exclusive_fp_after_loading = True\n\n            # reset buffered io handle in case fp\n            # was passed to libtiff, invalidating the buffer\n            self.fp.tell()\n\n            # load IFD data from fp before it is closed\n            exif = self.getexif()\n            for key in TiffTags.TAGS_V2_GROUPS.keys():\n                if key not in exif:\n                    continue\n                exif.get_ifd(key)\n\n    def _load_libtiff(self):\n        \"\"\"Overload method triggered when we detect a compressed tiff\n        Calls out to libtiff\"\"\"\n\n        Image.Image.load(self)\n\n        self.load_prepare()\n\n        if not len(self.tile) == 1:\n            raise OSError(\"Not exactly one tile\")\n\n        # (self._compression, (extents tuple),\n        #   0, (rawmode, self._compression, fp))\n        extents = self.tile[0][1]\n        args = list(self.tile[0][3])\n\n        # To be nice on memory footprint, if there's a\n        # file descriptor, use that instead of reading\n        # into a string in python.\n        # libtiff closes the file descriptor, so pass in a dup.\n        try:\n            fp = hasattr(self.fp, \"fileno\") and os.dup(self.fp.fileno())\n            # flush the file descriptor, prevents error on pypy 2.4+\n            # should also eliminate the need for fp.tell\n            # in _seek\n            if hasattr(self.fp, \"flush\"):\n                self.fp.flush()\n        except OSError:\n            # io.BytesIO have a fileno, but returns an OSError if\n            # it doesn't use a file descriptor.\n            fp = False\n\n        if fp:\n            args[2] = fp\n\n        decoder = Image._getdecoder(\n            self.mode, \"libtiff\", tuple(args), self.decoderconfig\n        )\n        try:\n            decoder.setimage(self.im, extents)\n        except ValueError as e:\n            raise OSError(\"Couldn't set the image\") from e\n\n        close_self_fp = self._exclusive_fp and not self.is_animated\n        if hasattr(self.fp, \"getvalue\"):\n            # We've got a stringio like thing passed in. Yay for all in memory.\n            # The decoder needs the entire file in one shot, so there's not\n            # a lot we can do here other than give it the entire file.\n            # unless we could do something like get the address of the\n            # underlying string for stringio.\n            #\n            # Rearranging for supporting byteio items, since they have a fileno\n            # that returns an OSError if there's no underlying fp. Easier to\n            # deal with here by reordering.\n            logger.debug(\"have getvalue. just sending in a string from getvalue\")\n            n, err = decoder.decode(self.fp.getvalue())\n        elif fp:\n            # we've got a actual file on disk, pass in the fp.\n            logger.debug(\"have fileno, calling fileno version of the decoder.\")\n            if not close_self_fp:\n                self.fp.seek(0)\n            # 4 bytes, otherwise the trace might error out\n            n, err = decoder.decode(b\"fpfp\")\n        else:\n            # we have something else.\n            logger.debug(\"don't have fileno or getvalue. just reading\")\n            self.fp.seek(0)\n            # UNDONE -- so much for that buffer size thing.\n            n, err = decoder.decode(self.fp.read())\n\n        if fp:\n            try:\n                os.close(fp)\n            except OSError:\n                pass\n\n        self.tile = []\n        self.readonly = 0\n\n        self.load_end()\n\n        # libtiff closed the fp in a, we need to close self.fp, if possible\n        if close_self_fp:\n            self.fp.close()\n            self.fp = None  # might be shared\n\n        if err < 0:\n            raise OSError(err)\n\n        return Image.Image.load(self)\n\n    def _setup(self):\n        \"\"\"Setup this image object based on current tags\"\"\"\n\n        if 0xBC01 in self.tag_v2:\n            raise OSError(\"Windows Media Photo files not yet supported\")\n\n        # extract relevant tags\n        self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]\n        self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)\n\n        # photometric is a required tag, but not everyone is reading\n        # the specification\n        photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)\n\n        # old style jpeg compression images most certainly are YCbCr\n        if self._compression == \"tiff_jpeg\":\n            photo = 6\n\n        fillorder = self.tag_v2.get(FILLORDER, 1)\n\n        logger.debug(\"*** Summary ***\")\n        logger.debug(f\"- compression: {self._compression}\")\n        logger.debug(f\"- photometric_interpretation: {photo}\")\n        logger.debug(f\"- planar_configuration: {self._planar_configuration}\")\n        logger.debug(f\"- fill_order: {fillorder}\")\n        logger.debug(f\"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}\")\n\n        # size\n        xsize = int(self.tag_v2.get(IMAGEWIDTH))\n        ysize = int(self.tag_v2.get(IMAGELENGTH))\n        self._size = xsize, ysize\n\n        logger.debug(f\"- size: {self.size}\")\n\n        sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))\n        if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:\n            # SAMPLEFORMAT is properly per band, so an RGB image will\n            # be (1,1,1).  But, we don't support per band pixel types,\n            # and anything more than one band is a uint8. So, just\n            # take the first element. Revisit this if adding support\n            # for more exotic images.\n            sample_format = (1,)\n\n        bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))\n        extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())\n        if photo in (2, 6, 8):  # RGB, YCbCr, LAB\n            bps_count = 3\n        elif photo == 5:  # CMYK\n            bps_count = 4\n        else:\n            bps_count = 1\n        bps_count += len(extra_tuple)\n        bps_actual_count = len(bps_tuple)\n        samples_per_pixel = self.tag_v2.get(\n            SAMPLESPERPIXEL,\n            3 if self._compression == \"tiff_jpeg\" and photo in (2, 6) else 1,\n        )\n\n        if samples_per_pixel > MAX_SAMPLESPERPIXEL:\n            # DOS check, samples_per_pixel can be a Long, and we extend the tuple below\n            logger.error(\n                \"More samples per pixel than can be decoded: %s\", samples_per_pixel\n            )\n            raise SyntaxError(\"Invalid value for samples per pixel\")\n\n        if samples_per_pixel < bps_actual_count:\n            # If a file has more values in bps_tuple than expected,\n            # remove the excess.\n            bps_tuple = bps_tuple[:samples_per_pixel]\n        elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:\n            # If a file has only one value in bps_tuple, when it should have more,\n            # presume it is the same number of bits for all of the samples.\n            bps_tuple = bps_tuple * samples_per_pixel\n\n        if len(bps_tuple) != samples_per_pixel:\n            raise SyntaxError(\"unknown data organization\")\n\n        # mode: check photometric interpretation and bits per pixel\n        key = (\n            self.tag_v2.prefix,\n            photo,\n            sample_format,\n            fillorder,\n            bps_tuple,\n            extra_tuple,\n        )\n        logger.debug(f\"format key: {key}\")\n        try:\n            self.mode, rawmode = OPEN_INFO[key]\n        except KeyError as e:\n            logger.debug(\"- unsupported format\")\n            raise SyntaxError(\"unknown pixel mode\") from e\n\n        logger.debug(f\"- raw mode: {rawmode}\")\n        logger.debug(f\"- pil mode: {self.mode}\")\n\n        self.info[\"compression\"] = self._compression\n\n        xres = self.tag_v2.get(X_RESOLUTION, 1)\n        yres = self.tag_v2.get(Y_RESOLUTION, 1)\n\n        if xres and yres:\n            resunit = self.tag_v2.get(RESOLUTION_UNIT)\n            if resunit == 2:  # dots per inch\n                self.info[\"dpi\"] = (xres, yres)\n            elif resunit == 3:  # dots per centimeter. convert to dpi\n                self.info[\"dpi\"] = (xres * 2.54, yres * 2.54)\n            elif resunit is None:  # used to default to 1, but now 2)\n                self.info[\"dpi\"] = (xres, yres)\n                # For backward compatibility,\n                # we also preserve the old behavior\n                self.info[\"resolution\"] = xres, yres\n            else:  # No absolute unit of measurement\n                self.info[\"resolution\"] = xres, yres\n\n        # build tile descriptors\n        x = y = layer = 0\n        self.tile = []\n        self.use_load_libtiff = READ_LIBTIFF or self._compression != \"raw\"\n        if self.use_load_libtiff:\n            # Decoder expects entire file as one tile.\n            # There's a buffer size limit in load (64k)\n            # so large g4 images will fail if we use that\n            # function.\n            #\n            # Setup the one tile for the whole image, then\n            # use the _load_libtiff function.\n\n            # libtiff handles the fillmode for us, so 1;IR should\n            # actually be 1;I. Including the R double reverses the\n            # bits, so stripes of the image are reversed.  See\n            # https://github.com/python-pillow/Pillow/issues/279\n            if fillorder == 2:\n                # Replace fillorder with fillorder=1\n                key = key[:3] + (1,) + key[4:]\n                logger.debug(f\"format key: {key}\")\n                # this should always work, since all the\n                # fillorder==2 modes have a corresponding\n                # fillorder=1 mode\n                self.mode, rawmode = OPEN_INFO[key]\n            # libtiff always returns the bytes in native order.\n            # we're expecting image byte order. So, if the rawmode\n            # contains I;16, we need to convert from native to image\n            # byte order.\n            if rawmode == \"I;16\":\n                rawmode = \"I;16N\"\n            if \";16B\" in rawmode:\n                rawmode = rawmode.replace(\";16B\", \";16N\")\n            if \";16L\" in rawmode:\n                rawmode = rawmode.replace(\";16L\", \";16N\")\n\n            # YCbCr images with new jpeg compression with pixels in one plane\n            # unpacked straight into RGB values\n            if (\n                photo == 6\n                and self._compression == \"jpeg\"\n                and self._planar_configuration == 1\n            ):\n                rawmode = \"RGB\"\n\n            # Offset in the tile tuple is 0, we go from 0,0 to\n            # w,h, and we only do this once -- eds\n            a = (rawmode, self._compression, False, self.tag_v2.offset)\n            self.tile.append((\"libtiff\", (0, 0, xsize, ysize), 0, a))\n\n        elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:\n            # striped image\n            if STRIPOFFSETS in self.tag_v2:\n                offsets = self.tag_v2[STRIPOFFSETS]\n                h = self.tag_v2.get(ROWSPERSTRIP, ysize)\n                w = self.size[0]\n            else:\n                # tiled image\n                offsets = self.tag_v2[TILEOFFSETS]\n                w = self.tag_v2.get(TILEWIDTH)\n                h = self.tag_v2.get(TILELENGTH)\n\n            for offset in offsets:\n                if x + w > xsize:\n                    stride = w * sum(bps_tuple) / 8  # bytes per line\n                else:\n                    stride = 0\n\n                tile_rawmode = rawmode\n                if self._planar_configuration == 2:\n                    # each band on it's own layer\n                    tile_rawmode = rawmode[layer]\n                    # adjust stride width accordingly\n                    stride /= bps_count\n\n                a = (tile_rawmode, int(stride), 1)\n                self.tile.append(\n                    (\n                        self._compression,\n                        (x, y, min(x + w, xsize), min(y + h, ysize)),\n                        offset,\n                        a,\n                    )\n                )\n                x = x + w\n                if x >= self.size[0]:\n                    x, y = 0, y + h\n                    if y >= self.size[1]:\n                        x = y = 0\n                        layer += 1\n        else:\n            logger.debug(\"- unsupported data organization\")\n            raise SyntaxError(\"unknown data organization\")\n\n        # Fix up info.\n        if ICCPROFILE in self.tag_v2:\n            self.info[\"icc_profile\"] = self.tag_v2[ICCPROFILE]\n\n        # fixup palette descriptor\n\n        if self.mode in [\"P\", \"PA\"]:\n            palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]\n            self.palette = ImagePalette.raw(\"RGB;L\", b\"\".join(palette))\n\n        self._tile_orientation = self.tag_v2.get(0x0112)\n\n\n#\n# --------------------------------------------------------------------\n# Write TIFF files\n\n# little endian is default except for image modes with\n# explicit big endian byte-order\n\nSAVE_INFO = {\n    # mode => rawmode, byteorder, photometrics,\n    #           sampleformat, bitspersample, extra\n    \"1\": (\"1\", II, 1, 1, (1,), None),\n    \"L\": (\"L\", II, 1, 1, (8,), None),\n    \"LA\": (\"LA\", II, 1, 1, (8, 8), 2),\n    \"P\": (\"P\", II, 3, 1, (8,), None),\n    \"PA\": (\"PA\", II, 3, 1, (8, 8), 2),\n    \"I\": (\"I;32S\", II, 1, 2, (32,), None),\n    \"I;16\": (\"I;16\", II, 1, 1, (16,), None),\n    \"I;16S\": (\"I;16S\", II, 1, 2, (16,), None),\n    \"F\": (\"F;32F\", II, 1, 3, (32,), None),\n    \"RGB\": (\"RGB\", II, 2, 1, (8, 8, 8), None),\n    \"RGBX\": (\"RGBX\", II, 2, 1, (8, 8, 8, 8), 0),\n    \"RGBA\": (\"RGBA\", II, 2, 1, (8, 8, 8, 8), 2),\n    \"CMYK\": (\"CMYK\", II, 5, 1, (8, 8, 8, 8), None),\n    \"YCbCr\": (\"YCbCr\", II, 6, 1, (8, 8, 8), None),\n    \"LAB\": (\"LAB\", II, 8, 1, (8, 8, 8), None),\n    \"I;32BS\": (\"I;32BS\", MM, 1, 2, (32,), None),\n    \"I;16B\": (\"I;16B\", MM, 1, 1, (16,), None),\n    \"I;16BS\": (\"I;16BS\", MM, 1, 2, (16,), None),\n    \"F;32BF\": (\"F;32BF\", MM, 1, 3, (32,), None),\n}\n\n\ndef _save(im, fp, filename):\n\n    try:\n        rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]\n    except KeyError as e:\n        raise OSError(f\"cannot write mode {im.mode} as TIFF\") from e\n\n    ifd = ImageFileDirectory_v2(prefix=prefix)\n\n    encoderinfo = im.encoderinfo\n    encoderconfig = im.encoderconfig\n    try:\n        compression = encoderinfo[\"compression\"]\n    except KeyError:\n        compression = im.info.get(\"compression\")\n        if isinstance(compression, int):\n            # compression value may be from BMP. Ignore it\n            compression = None\n    if compression is None:\n        compression = \"raw\"\n    elif compression == \"tiff_jpeg\":\n        # OJPEG is obsolete, so use new-style JPEG compression instead\n        compression = \"jpeg\"\n    elif compression == \"tiff_deflate\":\n        compression = \"tiff_adobe_deflate\"\n\n    libtiff = WRITE_LIBTIFF or compression != \"raw\"\n\n    # required for color libtiff images\n    ifd[PLANAR_CONFIGURATION] = 1\n\n    ifd[IMAGEWIDTH] = im.size[0]\n    ifd[IMAGELENGTH] = im.size[1]\n\n    # write any arbitrary tags passed in as an ImageFileDirectory\n    if \"tiffinfo\" in encoderinfo:\n        info = encoderinfo[\"tiffinfo\"]\n    elif \"exif\" in encoderinfo:\n        info = encoderinfo[\"exif\"]\n        if isinstance(info, bytes):\n            exif = Image.Exif()\n            exif.load(info)\n            info = exif\n    else:\n        info = {}\n    logger.debug(\"Tiffinfo Keys: %s\" % list(info))\n    if isinstance(info, ImageFileDirectory_v1):\n        info = info.to_v2()\n    for key in info:\n        if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():\n            ifd[key] = info.get_ifd(key)\n        else:\n            ifd[key] = info.get(key)\n        try:\n            ifd.tagtype[key] = info.tagtype[key]\n        except Exception:\n            pass  # might not be an IFD. Might not have populated type\n\n    # additions written by Greg Couch, gregc@cgl.ucsf.edu\n    # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com\n    if hasattr(im, \"tag_v2\"):\n        # preserve tags from original TIFF image file\n        for key in (\n            RESOLUTION_UNIT,\n            X_RESOLUTION,\n            Y_RESOLUTION,\n            IPTC_NAA_CHUNK,\n            PHOTOSHOP_CHUNK,\n            XMP,\n        ):\n            if key in im.tag_v2:\n                ifd[key] = im.tag_v2[key]\n                ifd.tagtype[key] = im.tag_v2.tagtype[key]\n\n    # preserve ICC profile (should also work when saving other formats\n    # which support profiles as TIFF) -- 2008-06-06 Florian Hoech\n    icc = encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        ifd[ICCPROFILE] = icc\n\n    for key, name in [\n        (IMAGEDESCRIPTION, \"description\"),\n        (X_RESOLUTION, \"resolution\"),\n        (Y_RESOLUTION, \"resolution\"),\n        (X_RESOLUTION, \"x_resolution\"),\n        (Y_RESOLUTION, \"y_resolution\"),\n        (RESOLUTION_UNIT, \"resolution_unit\"),\n        (SOFTWARE, \"software\"),\n        (DATE_TIME, \"date_time\"),\n        (ARTIST, \"artist\"),\n        (COPYRIGHT, \"copyright\"),\n    ]:\n        if name in encoderinfo:\n            ifd[key] = encoderinfo[name]\n\n    dpi = encoderinfo.get(\"dpi\")\n    if dpi:\n        ifd[RESOLUTION_UNIT] = 2\n        ifd[X_RESOLUTION] = dpi[0]\n        ifd[Y_RESOLUTION] = dpi[1]\n\n    if bits != (1,):\n        ifd[BITSPERSAMPLE] = bits\n        if len(bits) != 1:\n            ifd[SAMPLESPERPIXEL] = len(bits)\n    if extra is not None:\n        ifd[EXTRASAMPLES] = extra\n    if format != 1:\n        ifd[SAMPLEFORMAT] = format\n\n    if PHOTOMETRIC_INTERPRETATION not in ifd:\n        ifd[PHOTOMETRIC_INTERPRETATION] = photo\n    elif im.mode in (\"1\", \"L\") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:\n        if im.mode == \"1\":\n            inverted_im = im.copy()\n            px = inverted_im.load()\n            for y in range(inverted_im.height):\n                for x in range(inverted_im.width):\n                    px[x, y] = 0 if px[x, y] == 255 else 255\n            im = inverted_im\n        else:\n            im = ImageOps.invert(im)\n\n    if im.mode in [\"P\", \"PA\"]:\n        lut = im.im.getpalette(\"RGB\", \"RGB;L\")\n        colormap = []\n        colors = len(lut) // 3\n        for i in range(3):\n            colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]\n            colormap += [0] * (256 - colors)\n        ifd[COLORMAP] = colormap\n    # data orientation\n    stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)\n    # aim for given strip size (64 KB by default) when using libtiff writer\n    if libtiff:\n        im_strip_size = encoderinfo.get(\"strip_size\", STRIP_SIZE)\n        rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])\n        # JPEG encoder expects multiple of 8 rows\n        if compression == \"jpeg\":\n            rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])\n    else:\n        rows_per_strip = im.size[1]\n    if rows_per_strip == 0:\n        rows_per_strip = 1\n    strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip\n    strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip\n    ifd[ROWSPERSTRIP] = rows_per_strip\n    if strip_byte_counts >= 2**16:\n        ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG\n    ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (\n        stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),\n    )\n    ifd[STRIPOFFSETS] = tuple(\n        range(0, strip_byte_counts * strips_per_image, strip_byte_counts)\n    )  # this is adjusted by IFD writer\n    # no compression by default:\n    ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)\n\n    if im.mode == \"YCbCr\":\n        for tag, value in {\n            YCBCRSUBSAMPLING: (1, 1),\n            REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),\n        }.items():\n            ifd.setdefault(tag, value)\n\n    blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]\n    if libtiff:\n        if \"quality\" in encoderinfo:\n            quality = encoderinfo[\"quality\"]\n            if not isinstance(quality, int) or quality < 0 or quality > 100:\n                raise ValueError(\"Invalid quality setting\")\n            if compression != \"jpeg\":\n                raise ValueError(\n                    \"quality setting only supported for 'jpeg' compression\"\n                )\n            ifd[JPEGQUALITY] = quality\n\n        logger.debug(\"Saving using libtiff encoder\")\n        logger.debug(\"Items: %s\" % sorted(ifd.items()))\n        _fp = 0\n        if hasattr(fp, \"fileno\"):\n            try:\n                fp.seek(0)\n                _fp = os.dup(fp.fileno())\n            except io.UnsupportedOperation:\n                pass\n\n        # optional types for non core tags\n        types = {}\n        # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library\n        # based on the data in the strip.\n        # The other tags expect arrays with a certain length (fixed or depending on\n        # BITSPERSAMPLE, etc), passing arrays with a different length will result in\n        # segfaults. Block these tags until we add extra validation.\n        # SUBIFD may also cause a segfault.\n        blocklist += [\n            REFERENCEBLACKWHITE,\n            STRIPBYTECOUNTS,\n            STRIPOFFSETS,\n            TRANSFERFUNCTION,\n            SUBIFD,\n        ]\n\n        # bits per sample is a single short in the tiff directory, not a list.\n        atts = {BITSPERSAMPLE: bits[0]}\n        # Merge the ones that we have with (optional) more bits from\n        # the original file, e.g x,y resolution so that we can\n        # save(load('')) == original file.\n        legacy_ifd = {}\n        if hasattr(im, \"tag\"):\n            legacy_ifd = im.tag.to_v2()\n\n        # SAMPLEFORMAT is determined by the image format and should not be copied\n        # from legacy_ifd.\n        supplied_tags = {**getattr(im, \"tag_v2\", {}), **legacy_ifd}\n        if SAMPLEFORMAT in supplied_tags:\n            del supplied_tags[SAMPLEFORMAT]\n\n        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):\n            # Libtiff can only process certain core items without adding\n            # them to the custom dictionary.\n            # Custom items are supported for int, float, unicode, string and byte\n            # values. Other types and tuples require a tagtype.\n            if tag not in TiffTags.LIBTIFF_CORE:\n                if not Image.core.libtiff_support_custom_tags:\n                    continue\n\n                if tag in ifd.tagtype:\n                    types[tag] = ifd.tagtype[tag]\n                elif not (isinstance(value, (int, float, str, bytes))):\n                    continue\n                else:\n                    type = TiffTags.lookup(tag).type\n                    if type:\n                        types[tag] = type\n            if tag not in atts and tag not in blocklist:\n                if isinstance(value, str):\n                    atts[tag] = value.encode(\"ascii\", \"replace\") + b\"\\0\"\n                elif isinstance(value, IFDRational):\n                    atts[tag] = float(value)\n                else:\n                    atts[tag] = value\n\n        if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:\n            atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]\n\n        logger.debug(\"Converted items: %s\" % sorted(atts.items()))\n\n        # libtiff always expects the bytes in native order.\n        # we're storing image byte order. So, if the rawmode\n        # contains I;16, we need to convert from native to image\n        # byte order.\n        if im.mode in (\"I;16B\", \"I;16\"):\n            rawmode = \"I;16N\"\n\n        # Pass tags as sorted list so that the tags are set in a fixed order.\n        # This is required by libtiff for some tags. For example, the JPEGQUALITY\n        # pseudo tag requires that the COMPRESS tag was already set.\n        tags = list(atts.items())\n        tags.sort()\n        a = (rawmode, compression, _fp, filename, tags, types)\n        e = Image._getencoder(im.mode, \"libtiff\", a, encoderconfig)\n        e.setimage(im.im, (0, 0) + im.size)\n        while True:\n            # undone, change to self.decodermaxblock:\n            l, s, d = e.encode(16 * 1024)\n            if not _fp:\n                fp.write(d)\n            if s:\n                break\n        if s < 0:\n            raise OSError(f\"encoder error {s} when writing image file\")\n\n    else:\n        for tag in blocklist:\n            del ifd[tag]\n        offset = ifd.save(fp)\n\n        ImageFile._save(\n            im, fp, [(\"raw\", (0, 0) + im.size, offset, (rawmode, stride, 1))]\n        )\n\n    # -- helper for multi-page save --\n    if \"_debug_multipage\" in encoderinfo:\n        # just to access o32 and o16 (using correct byte order)\n        im._debug_multipage = ifd\n\n\nclass AppendingTiffWriter:\n    fieldSizes = [\n        0,  # None\n        1,  # byte\n        1,  # ascii\n        2,  # short\n        4,  # long\n        8,  # rational\n        1,  # sbyte\n        1,  # undefined\n        2,  # sshort\n        4,  # slong\n        8,  # srational\n        4,  # float\n        8,  # double\n    ]\n\n    #    StripOffsets = 273\n    #    FreeOffsets = 288\n    #    TileOffsets = 324\n    #    JPEGQTables = 519\n    #    JPEGDCTables = 520\n    #    JPEGACTables = 521\n    Tags = {273, 288, 324, 519, 520, 521}\n\n    def __init__(self, fn, new=False):\n        if hasattr(fn, \"read\"):\n            self.f = fn\n            self.close_fp = False\n        else:\n            self.name = fn\n            self.close_fp = True\n            try:\n                self.f = open(fn, \"w+b\" if new else \"r+b\")\n            except OSError:\n                self.f = open(fn, \"w+b\")\n        self.beginning = self.f.tell()\n        self.setup()\n\n    def setup(self):\n        # Reset everything.\n        self.f.seek(self.beginning, os.SEEK_SET)\n\n        self.whereToWriteNewIFDOffset = None\n        self.offsetOfNewPage = 0\n\n        self.IIMM = iimm = self.f.read(4)\n        if not iimm:\n            # empty file - first page\n            self.isFirst = True\n            return\n\n        self.isFirst = False\n        if iimm == b\"II\\x2a\\x00\":\n            self.setEndian(\"<\")\n        elif iimm == b\"MM\\x00\\x2a\":\n            self.setEndian(\">\")\n        else:\n            raise RuntimeError(\"Invalid TIFF file header\")\n\n        self.skipIFDs()\n        self.goToEnd()\n\n    def finalize(self):\n        if self.isFirst:\n            return\n\n        # fix offsets\n        self.f.seek(self.offsetOfNewPage)\n\n        iimm = self.f.read(4)\n        if not iimm:\n            # raise RuntimeError(\"nothing written into new page\")\n            # Make it easy to finish a frame without committing to a new one.\n            return\n\n        if iimm != self.IIMM:\n            raise RuntimeError(\"IIMM of new page doesn't match IIMM of first page\")\n\n        ifd_offset = self.readLong()\n        ifd_offset += self.offsetOfNewPage\n        self.f.seek(self.whereToWriteNewIFDOffset)\n        self.writeLong(ifd_offset)\n        self.f.seek(ifd_offset)\n        self.fixIFD()\n\n    def newFrame(self):\n        # Call this to finish a frame.\n        self.finalize()\n        self.setup()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        if self.close_fp:\n            self.close()\n        return False\n\n    def tell(self):\n        return self.f.tell() - self.offsetOfNewPage\n\n    def seek(self, offset, whence=io.SEEK_SET):\n        if whence == os.SEEK_SET:\n            offset += self.offsetOfNewPage\n\n        self.f.seek(offset, whence)\n        return self.tell()\n\n    def goToEnd(self):\n        self.f.seek(0, os.SEEK_END)\n        pos = self.f.tell()\n\n        # pad to 16 byte boundary\n        pad_bytes = 16 - pos % 16\n        if 0 < pad_bytes < 16:\n            self.f.write(bytes(pad_bytes))\n        self.offsetOfNewPage = self.f.tell()\n\n    def setEndian(self, endian):\n        self.endian = endian\n        self.longFmt = self.endian + \"L\"\n        self.shortFmt = self.endian + \"H\"\n        self.tagFormat = self.endian + \"HHL\"\n\n    def skipIFDs(self):\n        while True:\n            ifd_offset = self.readLong()\n            if ifd_offset == 0:\n                self.whereToWriteNewIFDOffset = self.f.tell() - 4\n                break\n\n            self.f.seek(ifd_offset)\n            num_tags = self.readShort()\n            self.f.seek(num_tags * 12, os.SEEK_CUR)\n\n    def write(self, data):\n        return self.f.write(data)\n\n    def readShort(self):\n        (value,) = struct.unpack(self.shortFmt, self.f.read(2))\n        return value\n\n    def readLong(self):\n        (value,) = struct.unpack(self.longFmt, self.f.read(4))\n        return value\n\n    def rewriteLastShortToLong(self, value):\n        self.f.seek(-2, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def rewriteLastShort(self, value):\n        self.f.seek(-2, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.shortFmt, value))\n        if bytes_written is not None and bytes_written != 2:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 2\")\n\n    def rewriteLastLong(self, value):\n        self.f.seek(-4, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def writeShort(self, value):\n        bytes_written = self.f.write(struct.pack(self.shortFmt, value))\n        if bytes_written is not None and bytes_written != 2:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 2\")\n\n    def writeLong(self, value):\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def close(self):\n        self.finalize()\n        self.f.close()\n\n    def fixIFD(self):\n        num_tags = self.readShort()\n\n        for i in range(num_tags):\n            tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))\n\n            field_size = self.fieldSizes[field_type]\n            total_size = field_size * count\n            is_local = total_size <= 4\n            if not is_local:\n                offset = self.readLong()\n                offset += self.offsetOfNewPage\n                self.rewriteLastLong(offset)\n\n            if tag in self.Tags:\n                cur_pos = self.f.tell()\n\n                if is_local:\n                    self.fixOffsets(\n                        count, isShort=(field_size == 2), isLong=(field_size == 4)\n                    )\n                    self.f.seek(cur_pos + 4)\n                else:\n                    self.f.seek(offset)\n                    self.fixOffsets(\n                        count, isShort=(field_size == 2), isLong=(field_size == 4)\n                    )\n                    self.f.seek(cur_pos)\n\n                offset = cur_pos = None\n\n            elif is_local:\n                # skip the locally stored value that is not an offset\n                self.f.seek(4, os.SEEK_CUR)\n\n    def fixOffsets(self, count, isShort=False, isLong=False):\n        if not isShort and not isLong:\n            raise RuntimeError(\"offset is neither short nor long\")\n\n        for i in range(count):\n            offset = self.readShort() if isShort else self.readLong()\n            offset += self.offsetOfNewPage\n            if isShort and offset >= 65536:\n                # offset is now too large - we must convert shorts to longs\n                if count != 1:\n                    raise RuntimeError(\"not implemented\")  # XXX TODO\n\n                # simple case - the offset is just one and therefore it is\n                # local (not referenced with another offset)\n                self.rewriteLastShortToLong(offset)\n                self.f.seek(-10, os.SEEK_CUR)\n                self.writeShort(TiffTags.LONG)  # rewrite the type to LONG\n                self.f.seek(8, os.SEEK_CUR)\n            elif isShort:\n                self.rewriteLastShort(offset)\n            else:\n                self.rewriteLastLong(offset)\n\n\ndef _save_all(im, fp, filename):\n    encoderinfo = im.encoderinfo.copy()\n    encoderconfig = im.encoderconfig\n    append_images = list(encoderinfo.get(\"append_images\", []))\n    if not hasattr(im, \"n_frames\") and not append_images:\n        return _save(im, fp, filename)\n\n    cur_idx = im.tell()\n    try:\n        with AppendingTiffWriter(fp) as tf:\n            for ims in [im] + append_images:\n                ims.encoderinfo = encoderinfo\n                ims.encoderconfig = encoderconfig\n                if not hasattr(ims, \"n_frames\"):\n                    nfr = 1\n                else:\n                    nfr = ims.n_frames\n\n                for idx in range(nfr):\n                    ims.seek(idx)\n                    ims.load()\n                    _save(ims, tf, filename)\n                    tf.newFrame()\n    finally:\n        im.seek(cur_idx)\n\n\n#\n# --------------------------------------------------------------------\n# Register\n\nImage.register_open(TiffImageFile.format, TiffImageFile, _accept)\nImage.register_save(TiffImageFile.format, _save)\nImage.register_save_all(TiffImageFile.format, _save_all)\n\nImage.register_extensions(TiffImageFile.format, [\".tif\", \".tiff\"])\n\nImage.register_mime(TiffImageFile.format, \"image/tiff\")\n",
            "header": {
                "file": {
                    "path": "src/PIL/TiffImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "ab9ac5ea2",
                "new_commit_hash": "791e692c1",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/src/PIL/TiffImagePlugin.py"
            },
            "plus_file": {
                "path": "b/src/PIL/TiffImagePlugin.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 732,
                            "length": 6
                        },
                        "new_range": {
                            "start": 732,
                            "length": 8
                        },
                        "section": "class ImageFileDirectory_v2(MutableMapping):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "    @_register_writer(2)",
                                "type": "context"
                            },
                            {
                                "content": "    def write_string(self, value):",
                                "type": "context"
                            },
                            {
                                "content": "        # remerge of https://github.com/python-pillow/Pillow/pull/1416",
                                "type": "context"
                            },
                            {
                                "content": "        if isinstance(value, int):",
                                "type": "added"
                            },
                            {
                                "content": "            value = str(value)",
                                "type": "added"
                            },
                            {
                                "content": "        if not isinstance(value, bytes):",
                                "type": "context"
                            },
                            {
                                "content": "            value = value.encode(\"ascii\", \"replace\")",
                                "type": "context"
                            },
                            {
                                "content": "        return value + b\"\\0\"",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/TiffImagePlugin.py",
                            "type": "class",
                            "name": "ImageFileDirectory_v2",
                            "content": "class ImageFileDirectory_v2(MutableMapping):\n    \"\"\"This class represents a TIFF tag directory.  To speed things up, we\n    don't decode tags unless they're asked for.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v2()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        'Some Data'\n\n    Individual values are returned as the strings or numbers, sequences are\n    returned as tuples of the values.\n\n    The tiff metadata type of each item is stored in a dictionary of\n    tag types in\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types\n    are read from a tiff file, guessed from the type added, or added\n    manually.\n\n    Data Structures:\n\n        * ``self.tagtype = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: integer corresponding to the data type from\n            :py:data:`.TiffTags.TYPES`\n\n          .. versionadded:: 3.0.0\n\n    'Internal' data structures:\n\n        * ``self._tags_v2 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data, as tuple for multiple values\n\n        * ``self._tagdata = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: undecoded byte string from file\n\n        * ``self._tags_v1 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data in the v1 format\n\n    Tags will be found in the private attributes ``self._tagdata``, and in\n    ``self._tags_v2`` once decoded.\n\n    ``self.legacy_api`` is a value for internal use, and shouldn't be changed\n    from outside code. In cooperation with\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``\n    is true, then decoded tags will be populated into both ``_tags_v1`` and\n    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF\n    save routine. Tags should be read from ``_tags_v1`` if\n    ``legacy_api == true``.\n\n    \"\"\"\n\n    def __init__(self, ifh=b\"II\\052\\0\\0\\0\\0\\0\", prefix=None, group=None):\n        \"\"\"Initialize an ImageFileDirectory.\n\n        To construct an ImageFileDirectory from a real file, pass the 8-byte\n        magic header to the constructor.  To only set the endianness, pass it\n        as the 'prefix' keyword argument.\n\n        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets\n              endianness.\n        :param prefix: Override the endianness of the file.\n        \"\"\"\n        if not _accept(ifh):\n            raise SyntaxError(f\"not a TIFF file (header {repr(ifh)} not valid)\")\n        self._prefix = prefix if prefix is not None else ifh[:2]\n        if self._prefix == MM:\n            self._endian = \">\"\n        elif self._prefix == II:\n            self._endian = \"<\"\n        else:\n            raise SyntaxError(\"not a TIFF IFD\")\n        self._bigtiff = ifh[2] == 43\n        self.group = group\n        self.tagtype = {}\n        \"\"\" Dictionary of tag types \"\"\"\n        self.reset()\n        (self.next,) = (\n            self._unpack(\"Q\", ifh[8:]) if self._bigtiff else self._unpack(\"L\", ifh[4:])\n        )\n        self._legacy_api = False\n\n    prefix = property(lambda self: self._prefix)\n    offset = property(lambda self: self._offset)\n    legacy_api = property(lambda self: self._legacy_api)\n\n    @legacy_api.setter\n    def legacy_api(self, value):\n        raise Exception(\"Not allowing setting of legacy api\")\n\n    def reset(self):\n        self._tags_v1 = {}  # will remain empty if legacy_api is false\n        self._tags_v2 = {}  # main tag storage\n        self._tagdata = {}\n        self.tagtype = {}  # added 2008-06-05 by Florian Hoech\n        self._next = None\n        self._offset = None\n\n    def __str__(self):\n        return str(dict(self))\n\n    def named(self):\n        \"\"\"\n        :returns: dict of name|key: value\n\n        Returns the complete tag dictionary, with named tags where possible.\n        \"\"\"\n        return {\n            TiffTags.lookup(code, self.group).name: value\n            for code, value in self.items()\n        }\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v2))\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v2:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            self[tag] = handler(self, data, self.legacy_api)  # check type\n        val = self._tags_v2[tag]\n        if self.legacy_api and not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n    def __contains__(self, tag):\n        return tag in self._tags_v2 or tag in self._tagdata\n\n    def __setitem__(self, tag, value):\n        self._setitem(tag, value, self.legacy_api)\n\n    def _setitem(self, tag, value, legacy_api):\n        basetypes = (Number, bytes, str)\n\n        info = TiffTags.lookup(tag, self.group)\n        values = [value] if isinstance(value, basetypes) else value\n\n        if tag not in self.tagtype:\n            if info.type:\n                self.tagtype[tag] = info.type\n            else:\n                self.tagtype[tag] = TiffTags.UNDEFINED\n                if all(isinstance(v, IFDRational) for v in values):\n                    self.tagtype[tag] = (\n                        TiffTags.RATIONAL\n                        if all(v >= 0 for v in values)\n                        else TiffTags.SIGNED_RATIONAL\n                    )\n                elif all(isinstance(v, int) for v in values):\n                    if all(0 <= v < 2**16 for v in values):\n                        self.tagtype[tag] = TiffTags.SHORT\n                    elif all(-(2**15) < v < 2**15 for v in values):\n                        self.tagtype[tag] = TiffTags.SIGNED_SHORT\n                    else:\n                        self.tagtype[tag] = (\n                            TiffTags.LONG\n                            if all(v >= 0 for v in values)\n                            else TiffTags.SIGNED_LONG\n                        )\n                elif all(isinstance(v, float) for v in values):\n                    self.tagtype[tag] = TiffTags.DOUBLE\n                elif all(isinstance(v, str) for v in values):\n                    self.tagtype[tag] = TiffTags.ASCII\n                elif all(isinstance(v, bytes) for v in values):\n                    self.tagtype[tag] = TiffTags.BYTE\n\n        if self.tagtype[tag] == TiffTags.UNDEFINED:\n            values = [\n                v.encode(\"ascii\", \"replace\") if isinstance(v, str) else v\n                for v in values\n            ]\n        elif self.tagtype[tag] == TiffTags.RATIONAL:\n            values = [float(v) if isinstance(v, int) else v for v in values]\n\n        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)\n        if not is_ifd:\n            values = tuple(info.cvt_enum(value) for value in values)\n\n        dest = self._tags_v1 if legacy_api else self._tags_v2\n\n        # Three branches:\n        # Spec'd length == 1, Actual length 1, store as element\n        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.\n        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.\n        # Don't mess with the legacy api, since it's frozen.\n        if not is_ifd and (\n            (info.length == 1)\n            or self.tagtype[tag] == TiffTags.BYTE\n            or (info.length is None and len(values) == 1 and not legacy_api)\n        ):\n            # Don't mess with the legacy api, since it's frozen.\n            if legacy_api and self.tagtype[tag] in [\n                TiffTags.RATIONAL,\n                TiffTags.SIGNED_RATIONAL,\n            ]:  # rationals\n                values = (values,)\n            try:\n                (dest[tag],) = values\n            except ValueError:\n                # We've got a builtin tag with 1 expected entry\n                warnings.warn(\n                    f\"Metadata Warning, tag {tag} had too many entries: \"\n                    f\"{len(values)}, expected 1\"\n                )\n                dest[tag] = values[0]\n\n        else:\n            # Spec'd length > 1 or undefined\n            # Unspec'd, and length > 1\n            dest[tag] = values\n\n    def __delitem__(self, tag):\n        self._tags_v2.pop(tag, None)\n        self._tags_v1.pop(tag, None)\n        self._tagdata.pop(tag, None)\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v2))\n\n    def _unpack(self, fmt, data):\n        return struct.unpack(self._endian + fmt, data)\n\n    def _pack(self, fmt, *values):\n        return struct.pack(self._endian + fmt, *values)\n\n    def _register_loader(idx, size):\n        def decorator(func):\n            from .TiffTags import TYPES\n\n            if func.__name__.startswith(\"load_\"):\n                TYPES[idx] = func.__name__[5:].replace(\"_\", \" \")\n            _load_dispatch[idx] = size, func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_writer(idx):\n        def decorator(func):\n            _write_dispatch[idx] = func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_basic(idx_fmt_name):\n        from .TiffTags import TYPES\n\n        idx, fmt, name = idx_fmt_name\n        TYPES[idx] = name\n        size = struct.calcsize(\"=\" + fmt)\n        _load_dispatch[idx] = (  # noqa: F821\n            size,\n            lambda self, data, legacy_api=True: (\n                self._unpack(f\"{len(data) // size}{fmt}\", data)\n            ),\n        )\n        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821\n            b\"\".join(self._pack(fmt, value) for value in values)\n        )\n\n    list(\n        map(\n            _register_basic,\n            [\n                (TiffTags.SHORT, \"H\", \"short\"),\n                (TiffTags.LONG, \"L\", \"long\"),\n                (TiffTags.SIGNED_BYTE, \"b\", \"signed byte\"),\n                (TiffTags.SIGNED_SHORT, \"h\", \"signed short\"),\n                (TiffTags.SIGNED_LONG, \"l\", \"signed long\"),\n                (TiffTags.FLOAT, \"f\", \"float\"),\n                (TiffTags.DOUBLE, \"d\", \"double\"),\n                (TiffTags.IFD, \"L\", \"long\"),\n                (TiffTags.LONG8, \"Q\", \"long8\"),\n            ],\n        )\n    )\n\n    @_register_loader(1, 1)  # Basic type, except for the legacy API.\n    def load_byte(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(1)  # Basic type, except for the legacy API.\n    def write_byte(self, data):\n        if isinstance(data, int):\n            data = bytes((data,))\n        return data\n\n    @_register_loader(2, 1)\n    def load_string(self, data, legacy_api=True):\n        if data.endswith(b\"\\0\"):\n            data = data[:-1]\n        return data.decode(\"latin-1\", \"replace\")\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if isinstance(value, int):\n            value = str(value)\n        if not isinstance(value, bytes):\n            value = value.encode(\"ascii\", \"replace\")\n        return value + b\"\\0\"\n\n    @_register_loader(5, 8)\n    def load_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}L\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(5)\n    def write_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2L\", *_limit_rational(frac, 2**32 - 1)) for frac in values\n        )\n\n    @_register_loader(7, 1)\n    def load_undefined(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(7)\n    def write_undefined(self, value):\n        return value\n\n    @_register_loader(10, 8)\n    def load_signed_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}l\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(10)\n    def write_signed_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2l\", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))\n            for frac in values\n        )\n\n    def _ensure_read(self, fp, size):\n        ret = fp.read(size)\n        if len(ret) != size:\n            raise OSError(\n                \"Corrupt EXIF data.  \"\n                f\"Expecting to read {size} bytes but only got {len(ret)}. \"\n            )\n        return ret\n\n    def load(self, fp):\n\n        self.reset()\n        self._offset = fp.tell()\n\n        try:\n            tag_count = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"H\", self._ensure_read(fp, 2))\n            )[0]\n            for i in range(tag_count):\n                tag, typ, count, data = (\n                    self._unpack(\"HHQ8s\", self._ensure_read(fp, 20))\n                    if self._bigtiff\n                    else self._unpack(\"HHL4s\", self._ensure_read(fp, 12))\n                )\n\n                tagname = TiffTags.lookup(tag, self.group).name\n                typname = TYPES.get(typ, \"unknown\")\n                msg = f\"tag: {tagname} ({tag}) - type: {typname} ({typ})\"\n\n                try:\n                    unit_size, handler = self._load_dispatch[typ]\n                except KeyError:\n                    logger.debug(msg + f\" - unsupported type {typ}\")\n                    continue  # ignore unsupported type\n                size = count * unit_size\n                if size > (8 if self._bigtiff else 4):\n                    here = fp.tell()\n                    (offset,) = self._unpack(\"Q\" if self._bigtiff else \"L\", data)\n                    msg += f\" Tag Location: {here} - Data Location: {offset}\"\n                    fp.seek(offset)\n                    data = ImageFile._safe_read(fp, size)\n                    fp.seek(here)\n                else:\n                    data = data[:size]\n\n                if len(data) != size:\n                    warnings.warn(\n                        \"Possibly corrupt EXIF data.  \"\n                        f\"Expecting to read {size} bytes but only got {len(data)}.\"\n                        f\" Skipping tag {tag}\"\n                    )\n                    logger.debug(msg)\n                    continue\n\n                if not data:\n                    logger.debug(msg)\n                    continue\n\n                self._tagdata[tag] = data\n                self.tagtype[tag] = typ\n\n                msg += \" - value: \" + (\n                    \"<table: %d bytes>\" % size if size > 32 else repr(data)\n                )\n                logger.debug(msg)\n\n            (self.next,) = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"L\", self._ensure_read(fp, 4))\n            )\n        except OSError as msg:\n            warnings.warn(str(msg))\n            return\n\n    def tobytes(self, offset=0):\n        # FIXME What about tagdata?\n        result = self._pack(\"H\", len(self._tags_v2))\n\n        entries = []\n        offset = offset + len(result) + len(self._tags_v2) * 12 + 4\n        stripoffsets = None\n\n        # pass 1: convert tags to binary format\n        # always write tags in ascending order\n        for tag, value in sorted(self._tags_v2.items()):\n            if tag == STRIPOFFSETS:\n                stripoffsets = len(entries)\n            typ = self.tagtype.get(tag)\n            logger.debug(f\"Tag {tag}, Type: {typ}, Value: {repr(value)}\")\n            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)\n            if is_ifd:\n                if self._endian == \"<\":\n                    ifh = b\"II\\x2A\\x00\\x08\\x00\\x00\\x00\"\n                else:\n                    ifh = b\"MM\\x00\\x2A\\x00\\x00\\x00\\x08\"\n                ifd = ImageFileDirectory_v2(ifh, group=tag)\n                values = self._tags_v2[tag]\n                for ifd_tag, ifd_value in values.items():\n                    ifd[ifd_tag] = ifd_value\n                data = ifd.tobytes(offset)\n            else:\n                values = value if isinstance(value, tuple) else (value,)\n                data = self._write_dispatch[typ](self, *values)\n\n            tagname = TiffTags.lookup(tag, self.group).name\n            typname = \"ifd\" if is_ifd else TYPES.get(typ, \"unknown\")\n            msg = f\"save: {tagname} ({tag}) - type: {typname} ({typ})\"\n            msg += \" - value: \" + (\n                \"<table: %d bytes>\" % len(data) if len(data) >= 16 else str(values)\n            )\n            logger.debug(msg)\n\n            # count is sum of lengths for string and arbitrary data\n            if is_ifd:\n                count = 1\n            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:\n                count = len(data)\n            else:\n                count = len(values)\n            # figure out if data fits into the entry\n            if len(data) <= 4:\n                entries.append((tag, typ, count, data.ljust(4, b\"\\0\"), b\"\"))\n            else:\n                entries.append((tag, typ, count, self._pack(\"L\", offset), data))\n                offset += (len(data) + 1) // 2 * 2  # pad to word\n\n        # update strip offset data to point beyond auxiliary data\n        if stripoffsets is not None:\n            tag, typ, count, value, data = entries[stripoffsets]\n            if data:\n                raise NotImplementedError(\"multistrip support not yet implemented\")\n            value = self._pack(\"L\", self._unpack(\"L\", value)[0] + offset)\n            entries[stripoffsets] = tag, typ, count, value, data\n\n        # pass 2: write entries to file\n        for tag, typ, count, value, data in entries:\n            logger.debug(f\"{tag} {typ} {count} {repr(value)} {repr(data)}\")\n            result += self._pack(\"HHL4s\", tag, typ, count, value)\n\n        # -- overwrite here for multi-page --\n        result += b\"\\0\\0\\0\\0\"  # end of entries\n\n        # pass 3: write auxiliary data to file\n        for tag, typ, count, value, data in entries:\n            result += data\n            if len(data) & 1:\n                result += b\"\\0\"\n\n        return result\n\n    def save(self, fp):\n\n        if fp.tell() == 0:  # skip TIFF header on subsequent pages\n            # tiff header -- PIL always starts the first IFD at offset 8\n            fp.write(self._prefix + self._pack(\"HL\", 42, 8))\n\n        offset = fp.tell()\n        result = self.tobytes(offset)\n        fp.write(result)\n        return offset + len(result)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 430,
                            "end_lineno": 942,
                            "parent": null
                        },
                        {
                            "file_name": "src/PIL/TiffImagePlugin.py",
                            "type": "method",
                            "name": "ImageFileDirectory_v2.write_string",
                            "content": "class ImageFileDirectory_v2(MutableMapping):\n    \"\"\"This class represents a TIFF tag directory.  To speed things up, we\n    don't decode tags unless they're asked for.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v2()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        'Some Data'\n\n    Individual values are returned as the strings or numbers, sequences are\n    returned as tuples of the values.\n\n    The tiff metadata type of each item is stored in a dictionary of\n    tag types in\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types\n    are read from a tiff file, guessed from the type added, or added\n    manually.\n\n    Data Structures:\n\n        * ``self.tagtype = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: integer corresponding to the data type from\n            :py:data:`.TiffTags.TYPES`\n\n          .. versionadded:: 3.0.0\n\n    'Internal' data structures:\n\n        * ``self._tags_v2 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data, as tuple for multiple values\n\n        * ``self._tagdata = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: undecoded byte string from file\n\n        * ``self._tags_v1 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data in the v1 format\n\n    Tags will be found in the private attributes ``self._tagdata``, and in\n    ``self._tags_v2`` once decoded.\n\n    ``self.legacy_api`` is a value for internal use, and shouldn't be changed\n    from outside code. In cooperation with\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``\n    is true, then decoded tags will be populated into both ``_tags_v1`` and\n    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF\n    save routine. Tags should be read from ``_tags_v1`` if\n    ``legacy_api == true``.\n\n    \"\"\"\n\n    def __init__(self, ifh=b\"II\\052\\0\\0\\0\\0\\0\", prefix=None, group=None):\n        \"\"\"Initialize an ImageFileDirectory.\n\n        To construct an ImageFileDirectory from a real file, pass the 8-byte\n        magic header to the constructor.  To only set the endianness, pass it\n        as the 'prefix' keyword argument.\n\n        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets\n              endianness.\n        :param prefix: Override the endianness of the file.\n        \"\"\"\n        if not _accept(ifh):\n            raise SyntaxError(f\"not a TIFF file (header {repr(ifh)} not valid)\")\n        self._prefix = prefix if prefix is not None else ifh[:2]\n        if self._prefix == MM:\n            self._endian = \">\"\n        elif self._prefix == II:\n            self._endian = \"<\"\n        else:\n            raise SyntaxError(\"not a TIFF IFD\")\n        self._bigtiff = ifh[2] == 43\n        self.group = group\n        self.tagtype = {}\n        \"\"\" Dictionary of tag types \"\"\"\n        self.reset()\n        (self.next,) = (\n            self._unpack(\"Q\", ifh[8:]) if self._bigtiff else self._unpack(\"L\", ifh[4:])\n        )\n        self._legacy_api = False\n\n    prefix = property(lambda self: self._prefix)\n    offset = property(lambda self: self._offset)\n    legacy_api = property(lambda self: self._legacy_api)\n\n    @legacy_api.setter\n    def legacy_api(self, value):\n        raise Exception(\"Not allowing setting of legacy api\")\n\n    def reset(self):\n        self._tags_v1 = {}  # will remain empty if legacy_api is false\n        self._tags_v2 = {}  # main tag storage\n        self._tagdata = {}\n        self.tagtype = {}  # added 2008-06-05 by Florian Hoech\n        self._next = None\n        self._offset = None\n\n    def __str__(self):\n        return str(dict(self))\n\n    def named(self):\n        \"\"\"\n        :returns: dict of name|key: value\n\n        Returns the complete tag dictionary, with named tags where possible.\n        \"\"\"\n        return {\n            TiffTags.lookup(code, self.group).name: value\n            for code, value in self.items()\n        }\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v2))\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v2:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            self[tag] = handler(self, data, self.legacy_api)  # check type\n        val = self._tags_v2[tag]\n        if self.legacy_api and not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n    def __contains__(self, tag):\n        return tag in self._tags_v2 or tag in self._tagdata\n\n    def __setitem__(self, tag, value):\n        self._setitem(tag, value, self.legacy_api)\n\n    def _setitem(self, tag, value, legacy_api):\n        basetypes = (Number, bytes, str)\n\n        info = TiffTags.lookup(tag, self.group)\n        values = [value] if isinstance(value, basetypes) else value\n\n        if tag not in self.tagtype:\n            if info.type:\n                self.tagtype[tag] = info.type\n            else:\n                self.tagtype[tag] = TiffTags.UNDEFINED\n                if all(isinstance(v, IFDRational) for v in values):\n                    self.tagtype[tag] = (\n                        TiffTags.RATIONAL\n                        if all(v >= 0 for v in values)\n                        else TiffTags.SIGNED_RATIONAL\n                    )\n                elif all(isinstance(v, int) for v in values):\n                    if all(0 <= v < 2**16 for v in values):\n                        self.tagtype[tag] = TiffTags.SHORT\n                    elif all(-(2**15) < v < 2**15 for v in values):\n                        self.tagtype[tag] = TiffTags.SIGNED_SHORT\n                    else:\n                        self.tagtype[tag] = (\n                            TiffTags.LONG\n                            if all(v >= 0 for v in values)\n                            else TiffTags.SIGNED_LONG\n                        )\n                elif all(isinstance(v, float) for v in values):\n                    self.tagtype[tag] = TiffTags.DOUBLE\n                elif all(isinstance(v, str) for v in values):\n                    self.tagtype[tag] = TiffTags.ASCII\n                elif all(isinstance(v, bytes) for v in values):\n                    self.tagtype[tag] = TiffTags.BYTE\n\n        if self.tagtype[tag] == TiffTags.UNDEFINED:\n            values = [\n                v.encode(\"ascii\", \"replace\") if isinstance(v, str) else v\n                for v in values\n            ]\n        elif self.tagtype[tag] == TiffTags.RATIONAL:\n            values = [float(v) if isinstance(v, int) else v for v in values]\n\n        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)\n        if not is_ifd:\n            values = tuple(info.cvt_enum(value) for value in values)\n\n        dest = self._tags_v1 if legacy_api else self._tags_v2\n\n        # Three branches:\n        # Spec'd length == 1, Actual length 1, store as element\n        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.\n        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.\n        # Don't mess with the legacy api, since it's frozen.\n        if not is_ifd and (\n            (info.length == 1)\n            or self.tagtype[tag] == TiffTags.BYTE\n            or (info.length is None and len(values) == 1 and not legacy_api)\n        ):\n            # Don't mess with the legacy api, since it's frozen.\n            if legacy_api and self.tagtype[tag] in [\n                TiffTags.RATIONAL,\n                TiffTags.SIGNED_RATIONAL,\n            ]:  # rationals\n                values = (values,)\n            try:\n                (dest[tag],) = values\n            except ValueError:\n                # We've got a builtin tag with 1 expected entry\n                warnings.warn(\n                    f\"Metadata Warning, tag {tag} had too many entries: \"\n                    f\"{len(values)}, expected 1\"\n                )\n                dest[tag] = values[0]\n\n        else:\n            # Spec'd length > 1 or undefined\n            # Unspec'd, and length > 1\n            dest[tag] = values\n\n    def __delitem__(self, tag):\n        self._tags_v2.pop(tag, None)\n        self._tags_v1.pop(tag, None)\n        self._tagdata.pop(tag, None)\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v2))\n\n    def _unpack(self, fmt, data):\n        return struct.unpack(self._endian + fmt, data)\n\n    def _pack(self, fmt, *values):\n        return struct.pack(self._endian + fmt, *values)\n\n    def _register_loader(idx, size):\n        def decorator(func):\n            from .TiffTags import TYPES\n\n            if func.__name__.startswith(\"load_\"):\n                TYPES[idx] = func.__name__[5:].replace(\"_\", \" \")\n            _load_dispatch[idx] = size, func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_writer(idx):\n        def decorator(func):\n            _write_dispatch[idx] = func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_basic(idx_fmt_name):\n        from .TiffTags import TYPES\n\n        idx, fmt, name = idx_fmt_name\n        TYPES[idx] = name\n        size = struct.calcsize(\"=\" + fmt)\n        _load_dispatch[idx] = (  # noqa: F821\n            size,\n            lambda self, data, legacy_api=True: (\n                self._unpack(f\"{len(data) // size}{fmt}\", data)\n            ),\n        )\n        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821\n            b\"\".join(self._pack(fmt, value) for value in values)\n        )\n\n    list(\n        map(\n            _register_basic,\n            [\n                (TiffTags.SHORT, \"H\", \"short\"),\n                (TiffTags.LONG, \"L\", \"long\"),\n                (TiffTags.SIGNED_BYTE, \"b\", \"signed byte\"),\n                (TiffTags.SIGNED_SHORT, \"h\", \"signed short\"),\n                (TiffTags.SIGNED_LONG, \"l\", \"signed long\"),\n                (TiffTags.FLOAT, \"f\", \"float\"),\n                (TiffTags.DOUBLE, \"d\", \"double\"),\n                (TiffTags.IFD, \"L\", \"long\"),\n                (TiffTags.LONG8, \"Q\", \"long8\"),\n            ],\n        )\n    )\n\n    @_register_loader(1, 1)  # Basic type, except for the legacy API.\n    def load_byte(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(1)  # Basic type, except for the legacy API.\n    def write_byte(self, data):\n        if isinstance(data, int):\n            data = bytes((data,))\n        return data\n\n    @_register_loader(2, 1)\n    def load_string(self, data, legacy_api=True):\n        if data.endswith(b\"\\0\"):\n            data = data[:-1]\n        return data.decode(\"latin-1\", \"replace\")\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if isinstance(value, int):\n            value = str(value)\n        if not isinstance(value, bytes):\n            value = value.encode(\"ascii\", \"replace\")\n        return value + b\"\\0\"\n\n    @_register_loader(5, 8)\n    def load_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}L\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(5)\n    def write_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2L\", *_limit_rational(frac, 2**32 - 1)) for frac in values\n        )\n\n    @_register_loader(7, 1)\n    def load_undefined(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(7)\n    def write_undefined(self, value):\n        return value\n\n    @_register_loader(10, 8)\n    def load_signed_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}l\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(10)\n    def write_signed_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2l\", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))\n            for frac in values\n        )\n\n    def _ensure_read(self, fp, size):\n        ret = fp.read(size)\n        if len(ret) != size:\n            raise OSError(\n                \"Corrupt EXIF data.  \"\n                f\"Expecting to read {size} bytes but only got {len(ret)}. \"\n            )\n        return ret\n\n    def load(self, fp):\n\n        self.reset()\n        self._offset = fp.tell()\n\n        try:\n            tag_count = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"H\", self._ensure_read(fp, 2))\n            )[0]\n            for i in range(tag_count):\n                tag, typ, count, data = (\n                    self._unpack(\"HHQ8s\", self._ensure_read(fp, 20))\n                    if self._bigtiff\n                    else self._unpack(\"HHL4s\", self._ensure_read(fp, 12))\n                )\n\n                tagname = TiffTags.lookup(tag, self.group).name\n                typname = TYPES.get(typ, \"unknown\")\n                msg = f\"tag: {tagname} ({tag}) - type: {typname} ({typ})\"\n\n                try:\n                    unit_size, handler = self._load_dispatch[typ]\n                except KeyError:\n                    logger.debug(msg + f\" - unsupported type {typ}\")\n                    continue  # ignore unsupported type\n                size = count * unit_size\n                if size > (8 if self._bigtiff else 4):\n                    here = fp.tell()\n                    (offset,) = self._unpack(\"Q\" if self._bigtiff else \"L\", data)\n                    msg += f\" Tag Location: {here} - Data Location: {offset}\"\n                    fp.seek(offset)\n                    data = ImageFile._safe_read(fp, size)\n                    fp.seek(here)\n                else:\n                    data = data[:size]\n\n                if len(data) != size:\n                    warnings.warn(\n                        \"Possibly corrupt EXIF data.  \"\n                        f\"Expecting to read {size} bytes but only got {len(data)}.\"\n                        f\" Skipping tag {tag}\"\n                    )\n                    logger.debug(msg)\n                    continue\n\n                if not data:\n                    logger.debug(msg)\n                    continue\n\n                self._tagdata[tag] = data\n                self.tagtype[tag] = typ\n\n                msg += \" - value: \" + (\n                    \"<table: %d bytes>\" % size if size > 32 else repr(data)\n                )\n                logger.debug(msg)\n\n            (self.next,) = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"L\", self._ensure_read(fp, 4))\n            )\n        except OSError as msg:\n            warnings.warn(str(msg))\n            return\n\n    def tobytes(self, offset=0):\n        # FIXME What about tagdata?\n        result = self._pack(\"H\", len(self._tags_v2))\n\n        entries = []\n        offset = offset + len(result) + len(self._tags_v2) * 12 + 4\n        stripoffsets = None\n\n        # pass 1: convert tags to binary format\n        # always write tags in ascending order\n        for tag, value in sorted(self._tags_v2.items()):\n            if tag == STRIPOFFSETS:\n                stripoffsets = len(entries)\n            typ = self.tagtype.get(tag)\n            logger.debug(f\"Tag {tag}, Type: {typ}, Value: {repr(value)}\")\n            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)\n            if is_ifd:\n                if self._endian == \"<\":\n                    ifh = b\"II\\x2A\\x00\\x08\\x00\\x00\\x00\"\n                else:\n                    ifh = b\"MM\\x00\\x2A\\x00\\x00\\x00\\x08\"\n                ifd = ImageFileDirectory_v2(ifh, group=tag)\n                values = self._tags_v2[tag]\n                for ifd_tag, ifd_value in values.items():\n                    ifd[ifd_tag] = ifd_value\n                data = ifd.tobytes(offset)\n            else:\n                values = value if isinstance(value, tuple) else (value,)\n                data = self._write_dispatch[typ](self, *values)\n\n            tagname = TiffTags.lookup(tag, self.group).name\n            typname = \"ifd\" if is_ifd else TYPES.get(typ, \"unknown\")\n            msg = f\"save: {tagname} ({tag}) - type: {typname} ({typ})\"\n            msg += \" - value: \" + (\n                \"<table: %d bytes>\" % len(data) if len(data) >= 16 else str(values)\n            )\n            logger.debug(msg)\n\n            # count is sum of lengths for string and arbitrary data\n            if is_ifd:\n                count = 1\n            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:\n                count = len(data)\n            else:\n                count = len(values)\n            # figure out if data fits into the entry\n            if len(data) <= 4:\n                entries.append((tag, typ, count, data.ljust(4, b\"\\0\"), b\"\"))\n            else:\n                entries.append((tag, typ, count, self._pack(\"L\", offset), data))\n                offset += (len(data) + 1) // 2 * 2  # pad to word\n\n        # update strip offset data to point beyond auxiliary data\n        if stripoffsets is not None:\n            tag, typ, count, value, data = entries[stripoffsets]\n            if data:\n                raise NotImplementedError(\"multistrip support not yet implemented\")\n            value = self._pack(\"L\", self._unpack(\"L\", value)[0] + offset)\n            entries[stripoffsets] = tag, typ, count, value, data\n\n        # pass 2: write entries to file\n        for tag, typ, count, value, data in entries:\n            logger.debug(f\"{tag} {typ} {count} {repr(value)} {repr(data)}\")\n            result += self._pack(\"HHL4s\", tag, typ, count, value)\n\n        # -- overwrite here for multi-page --\n        result += b\"\\0\\0\\0\\0\"  # end of entries\n\n        # pass 3: write auxiliary data to file\n        for tag, typ, count, value, data in entries:\n            result += data\n            if len(data) & 1:\n                result += b\"\\0\"\n\n        return result\n\n    def save(self, fp):\n\n        if fp.tell() == 0:  # skip TIFF header on subsequent pages\n            # tiff header -- PIL always starts the first IFD at offset 8\n            fp.write(self._prefix + self._pack(\"HL\", 42, 8))\n\n        offset = fp.tell()\n        result = self.tobytes(offset)\n        fp.write(result)\n        return offset + len(result)",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 733,
                            "end_lineno": 739,
                            "parent": {
                                "file_name": "src/PIL/TiffImagePlugin.py",
                                "type": "class",
                                "name": "ImageFileDirectory_v2",
                                "content": "class ImageFileDirectory_v2(MutableMapping):\n    \"\"\"This class represents a TIFF tag directory.  To speed things up, we\n    don't decode tags unless they're asked for.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v2()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        'Some Data'\n\n    Individual values are returned as the strings or numbers, sequences are\n    returned as tuples of the values.\n\n    The tiff metadata type of each item is stored in a dictionary of\n    tag types in\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types\n    are read from a tiff file, guessed from the type added, or added\n    manually.\n\n    Data Structures:\n\n        * ``self.tagtype = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: integer corresponding to the data type from\n            :py:data:`.TiffTags.TYPES`\n\n          .. versionadded:: 3.0.0\n\n    'Internal' data structures:\n\n        * ``self._tags_v2 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data, as tuple for multiple values\n\n        * ``self._tagdata = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: undecoded byte string from file\n\n        * ``self._tags_v1 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data in the v1 format\n\n    Tags will be found in the private attributes ``self._tagdata``, and in\n    ``self._tags_v2`` once decoded.\n\n    ``self.legacy_api`` is a value for internal use, and shouldn't be changed\n    from outside code. In cooperation with\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``\n    is true, then decoded tags will be populated into both ``_tags_v1`` and\n    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF\n    save routine. Tags should be read from ``_tags_v1`` if\n    ``legacy_api == true``.\n\n    \"\"\"\n\n    def __init__(self, ifh=b\"II\\052\\0\\0\\0\\0\\0\", prefix=None, group=None):\n        \"\"\"Initialize an ImageFileDirectory.\n\n        To construct an ImageFileDirectory from a real file, pass the 8-byte\n        magic header to the constructor.  To only set the endianness, pass it\n        as the 'prefix' keyword argument.\n\n        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets\n              endianness.\n        :param prefix: Override the endianness of the file.\n        \"\"\"\n        if not _accept(ifh):\n            raise SyntaxError(f\"not a TIFF file (header {repr(ifh)} not valid)\")\n        self._prefix = prefix if prefix is not None else ifh[:2]\n        if self._prefix == MM:\n            self._endian = \">\"\n        elif self._prefix == II:\n            self._endian = \"<\"\n        else:\n            raise SyntaxError(\"not a TIFF IFD\")\n        self._bigtiff = ifh[2] == 43\n        self.group = group\n        self.tagtype = {}\n        \"\"\" Dictionary of tag types \"\"\"\n        self.reset()\n        (self.next,) = (\n            self._unpack(\"Q\", ifh[8:]) if self._bigtiff else self._unpack(\"L\", ifh[4:])\n        )\n        self._legacy_api = False\n\n    prefix = property(lambda self: self._prefix)\n    offset = property(lambda self: self._offset)\n    legacy_api = property(lambda self: self._legacy_api)\n\n    @legacy_api.setter\n    def legacy_api(self, value):\n        raise Exception(\"Not allowing setting of legacy api\")\n\n    def reset(self):\n        self._tags_v1 = {}  # will remain empty if legacy_api is false\n        self._tags_v2 = {}  # main tag storage\n        self._tagdata = {}\n        self.tagtype = {}  # added 2008-06-05 by Florian Hoech\n        self._next = None\n        self._offset = None\n\n    def __str__(self):\n        return str(dict(self))\n\n    def named(self):\n        \"\"\"\n        :returns: dict of name|key: value\n\n        Returns the complete tag dictionary, with named tags where possible.\n        \"\"\"\n        return {\n            TiffTags.lookup(code, self.group).name: value\n            for code, value in self.items()\n        }\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v2))\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v2:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            self[tag] = handler(self, data, self.legacy_api)  # check type\n        val = self._tags_v2[tag]\n        if self.legacy_api and not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n    def __contains__(self, tag):\n        return tag in self._tags_v2 or tag in self._tagdata\n\n    def __setitem__(self, tag, value):\n        self._setitem(tag, value, self.legacy_api)\n\n    def _setitem(self, tag, value, legacy_api):\n        basetypes = (Number, bytes, str)\n\n        info = TiffTags.lookup(tag, self.group)\n        values = [value] if isinstance(value, basetypes) else value\n\n        if tag not in self.tagtype:\n            if info.type:\n                self.tagtype[tag] = info.type\n            else:\n                self.tagtype[tag] = TiffTags.UNDEFINED\n                if all(isinstance(v, IFDRational) for v in values):\n                    self.tagtype[tag] = (\n                        TiffTags.RATIONAL\n                        if all(v >= 0 for v in values)\n                        else TiffTags.SIGNED_RATIONAL\n                    )\n                elif all(isinstance(v, int) for v in values):\n                    if all(0 <= v < 2**16 for v in values):\n                        self.tagtype[tag] = TiffTags.SHORT\n                    elif all(-(2**15) < v < 2**15 for v in values):\n                        self.tagtype[tag] = TiffTags.SIGNED_SHORT\n                    else:\n                        self.tagtype[tag] = (\n                            TiffTags.LONG\n                            if all(v >= 0 for v in values)\n                            else TiffTags.SIGNED_LONG\n                        )\n                elif all(isinstance(v, float) for v in values):\n                    self.tagtype[tag] = TiffTags.DOUBLE\n                elif all(isinstance(v, str) for v in values):\n                    self.tagtype[tag] = TiffTags.ASCII\n                elif all(isinstance(v, bytes) for v in values):\n                    self.tagtype[tag] = TiffTags.BYTE\n\n        if self.tagtype[tag] == TiffTags.UNDEFINED:\n            values = [\n                v.encode(\"ascii\", \"replace\") if isinstance(v, str) else v\n                for v in values\n            ]\n        elif self.tagtype[tag] == TiffTags.RATIONAL:\n            values = [float(v) if isinstance(v, int) else v for v in values]\n\n        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)\n        if not is_ifd:\n            values = tuple(info.cvt_enum(value) for value in values)\n\n        dest = self._tags_v1 if legacy_api else self._tags_v2\n\n        # Three branches:\n        # Spec'd length == 1, Actual length 1, store as element\n        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.\n        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.\n        # Don't mess with the legacy api, since it's frozen.\n        if not is_ifd and (\n            (info.length == 1)\n            or self.tagtype[tag] == TiffTags.BYTE\n            or (info.length is None and len(values) == 1 and not legacy_api)\n        ):\n            # Don't mess with the legacy api, since it's frozen.\n            if legacy_api and self.tagtype[tag] in [\n                TiffTags.RATIONAL,\n                TiffTags.SIGNED_RATIONAL,\n            ]:  # rationals\n                values = (values,)\n            try:\n                (dest[tag],) = values\n            except ValueError:\n                # We've got a builtin tag with 1 expected entry\n                warnings.warn(\n                    f\"Metadata Warning, tag {tag} had too many entries: \"\n                    f\"{len(values)}, expected 1\"\n                )\n                dest[tag] = values[0]\n\n        else:\n            # Spec'd length > 1 or undefined\n            # Unspec'd, and length > 1\n            dest[tag] = values\n\n    def __delitem__(self, tag):\n        self._tags_v2.pop(tag, None)\n        self._tags_v1.pop(tag, None)\n        self._tagdata.pop(tag, None)\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v2))\n\n    def _unpack(self, fmt, data):\n        return struct.unpack(self._endian + fmt, data)\n\n    def _pack(self, fmt, *values):\n        return struct.pack(self._endian + fmt, *values)\n\n    def _register_loader(idx, size):\n        def decorator(func):\n            from .TiffTags import TYPES\n\n            if func.__name__.startswith(\"load_\"):\n                TYPES[idx] = func.__name__[5:].replace(\"_\", \" \")\n            _load_dispatch[idx] = size, func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_writer(idx):\n        def decorator(func):\n            _write_dispatch[idx] = func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_basic(idx_fmt_name):\n        from .TiffTags import TYPES\n\n        idx, fmt, name = idx_fmt_name\n        TYPES[idx] = name\n        size = struct.calcsize(\"=\" + fmt)\n        _load_dispatch[idx] = (  # noqa: F821\n            size,\n            lambda self, data, legacy_api=True: (\n                self._unpack(f\"{len(data) // size}{fmt}\", data)\n            ),\n        )\n        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821\n            b\"\".join(self._pack(fmt, value) for value in values)\n        )\n\n    list(\n        map(\n            _register_basic,\n            [\n                (TiffTags.SHORT, \"H\", \"short\"),\n                (TiffTags.LONG, \"L\", \"long\"),\n                (TiffTags.SIGNED_BYTE, \"b\", \"signed byte\"),\n                (TiffTags.SIGNED_SHORT, \"h\", \"signed short\"),\n                (TiffTags.SIGNED_LONG, \"l\", \"signed long\"),\n                (TiffTags.FLOAT, \"f\", \"float\"),\n                (TiffTags.DOUBLE, \"d\", \"double\"),\n                (TiffTags.IFD, \"L\", \"long\"),\n                (TiffTags.LONG8, \"Q\", \"long8\"),\n            ],\n        )\n    )\n\n    @_register_loader(1, 1)  # Basic type, except for the legacy API.\n    def load_byte(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(1)  # Basic type, except for the legacy API.\n    def write_byte(self, data):\n        if isinstance(data, int):\n            data = bytes((data,))\n        return data\n\n    @_register_loader(2, 1)\n    def load_string(self, data, legacy_api=True):\n        if data.endswith(b\"\\0\"):\n            data = data[:-1]\n        return data.decode(\"latin-1\", \"replace\")\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if isinstance(value, int):\n            value = str(value)\n        if not isinstance(value, bytes):\n            value = value.encode(\"ascii\", \"replace\")\n        return value + b\"\\0\"\n\n    @_register_loader(5, 8)\n    def load_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}L\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(5)\n    def write_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2L\", *_limit_rational(frac, 2**32 - 1)) for frac in values\n        )\n\n    @_register_loader(7, 1)\n    def load_undefined(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(7)\n    def write_undefined(self, value):\n        return value\n\n    @_register_loader(10, 8)\n    def load_signed_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}l\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(10)\n    def write_signed_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2l\", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))\n            for frac in values\n        )\n\n    def _ensure_read(self, fp, size):\n        ret = fp.read(size)\n        if len(ret) != size:\n            raise OSError(\n                \"Corrupt EXIF data.  \"\n                f\"Expecting to read {size} bytes but only got {len(ret)}. \"\n            )\n        return ret\n\n    def load(self, fp):\n\n        self.reset()\n        self._offset = fp.tell()\n\n        try:\n            tag_count = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"H\", self._ensure_read(fp, 2))\n            )[0]\n            for i in range(tag_count):\n                tag, typ, count, data = (\n                    self._unpack(\"HHQ8s\", self._ensure_read(fp, 20))\n                    if self._bigtiff\n                    else self._unpack(\"HHL4s\", self._ensure_read(fp, 12))\n                )\n\n                tagname = TiffTags.lookup(tag, self.group).name\n                typname = TYPES.get(typ, \"unknown\")\n                msg = f\"tag: {tagname} ({tag}) - type: {typname} ({typ})\"\n\n                try:\n                    unit_size, handler = self._load_dispatch[typ]\n                except KeyError:\n                    logger.debug(msg + f\" - unsupported type {typ}\")\n                    continue  # ignore unsupported type\n                size = count * unit_size\n                if size > (8 if self._bigtiff else 4):\n                    here = fp.tell()\n                    (offset,) = self._unpack(\"Q\" if self._bigtiff else \"L\", data)\n                    msg += f\" Tag Location: {here} - Data Location: {offset}\"\n                    fp.seek(offset)\n                    data = ImageFile._safe_read(fp, size)\n                    fp.seek(here)\n                else:\n                    data = data[:size]\n\n                if len(data) != size:\n                    warnings.warn(\n                        \"Possibly corrupt EXIF data.  \"\n                        f\"Expecting to read {size} bytes but only got {len(data)}.\"\n                        f\" Skipping tag {tag}\"\n                    )\n                    logger.debug(msg)\n                    continue\n\n                if not data:\n                    logger.debug(msg)\n                    continue\n\n                self._tagdata[tag] = data\n                self.tagtype[tag] = typ\n\n                msg += \" - value: \" + (\n                    \"<table: %d bytes>\" % size if size > 32 else repr(data)\n                )\n                logger.debug(msg)\n\n            (self.next,) = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"L\", self._ensure_read(fp, 4))\n            )\n        except OSError as msg:\n            warnings.warn(str(msg))\n            return\n\n    def tobytes(self, offset=0):\n        # FIXME What about tagdata?\n        result = self._pack(\"H\", len(self._tags_v2))\n\n        entries = []\n        offset = offset + len(result) + len(self._tags_v2) * 12 + 4\n        stripoffsets = None\n\n        # pass 1: convert tags to binary format\n        # always write tags in ascending order\n        for tag, value in sorted(self._tags_v2.items()):\n            if tag == STRIPOFFSETS:\n                stripoffsets = len(entries)\n            typ = self.tagtype.get(tag)\n            logger.debug(f\"Tag {tag}, Type: {typ}, Value: {repr(value)}\")\n            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)\n            if is_ifd:\n                if self._endian == \"<\":\n                    ifh = b\"II\\x2A\\x00\\x08\\x00\\x00\\x00\"\n                else:\n                    ifh = b\"MM\\x00\\x2A\\x00\\x00\\x00\\x08\"\n                ifd = ImageFileDirectory_v2(ifh, group=tag)\n                values = self._tags_v2[tag]\n                for ifd_tag, ifd_value in values.items():\n                    ifd[ifd_tag] = ifd_value\n                data = ifd.tobytes(offset)\n            else:\n                values = value if isinstance(value, tuple) else (value,)\n                data = self._write_dispatch[typ](self, *values)\n\n            tagname = TiffTags.lookup(tag, self.group).name\n            typname = \"ifd\" if is_ifd else TYPES.get(typ, \"unknown\")\n            msg = f\"save: {tagname} ({tag}) - type: {typname} ({typ})\"\n            msg += \" - value: \" + (\n                \"<table: %d bytes>\" % len(data) if len(data) >= 16 else str(values)\n            )\n            logger.debug(msg)\n\n            # count is sum of lengths for string and arbitrary data\n            if is_ifd:\n                count = 1\n            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:\n                count = len(data)\n            else:\n                count = len(values)\n            # figure out if data fits into the entry\n            if len(data) <= 4:\n                entries.append((tag, typ, count, data.ljust(4, b\"\\0\"), b\"\"))\n            else:\n                entries.append((tag, typ, count, self._pack(\"L\", offset), data))\n                offset += (len(data) + 1) // 2 * 2  # pad to word\n\n        # update strip offset data to point beyond auxiliary data\n        if stripoffsets is not None:\n            tag, typ, count, value, data = entries[stripoffsets]\n            if data:\n                raise NotImplementedError(\"multistrip support not yet implemented\")\n            value = self._pack(\"L\", self._unpack(\"L\", value)[0] + offset)\n            entries[stripoffsets] = tag, typ, count, value, data\n\n        # pass 2: write entries to file\n        for tag, typ, count, value, data in entries:\n            logger.debug(f\"{tag} {typ} {count} {repr(value)} {repr(data)}\")\n            result += self._pack(\"HHL4s\", tag, typ, count, value)\n\n        # -- overwrite here for multi-page --\n        result += b\"\\0\\0\\0\\0\"  # end of entries\n\n        # pass 3: write auxiliary data to file\n        for tag, typ, count, value, data in entries:\n            result += data\n            if len(data) & 1:\n                result += b\"\\0\"\n\n        return result\n\n    def save(self, fp):\n\n        if fp.tell() == 0:  # skip TIFF header on subsequent pages\n            # tiff header -- PIL always starts the first IFD at offset 8\n            fp.write(self._prefix + self._pack(\"HL\", 42, 8))\n\n        offset = fp.tell()\n        result = self.tobytes(offset)\n        fp.write(result)\n        return offset + len(result)",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 430,
                                "end_lineno": 942,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "6da4169f3724ffe20c72d8ef4a2e0dc21815b343^",
    "new_commit_hash": "6da4169f3724ffe20c72d8ef4a2e0dc21815b343",
    "commit_message": "Fixed writing int as ASCII tag",
    "commit_date": "2022-12-13T22:40:55+11:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "6da4169f3724ffe20c72d8ef4a2e0dc21815b343",
    "test_file_codes": [
        "import io\nimport struct\n\nimport pytest\n\nfrom PIL import Image, TiffImagePlugin, TiffTags\nfrom PIL.TiffImagePlugin import IFDRational\n\nfrom .helper import assert_deep_equal, hopper\n\nTAG_IDS = {info.name: info.value for info in TiffTags.TAGS_V2.values()}\n\n\ndef test_rt_metadata(tmp_path):\n    \"\"\"Test writing arbitrary metadata into the tiff image directory\n    Use case is ImageJ private tags, one numeric, one arbitrary\n    data.  https://github.com/python-pillow/Pillow/issues/291\n    \"\"\"\n\n    img = hopper()\n\n    # Behaviour change: re #1416\n    # Pre ifd rewrite, ImageJMetaData was being written as a string(2),\n    # Post ifd rewrite, it's defined as arbitrary bytes(7). It should\n    # roundtrip with the actual bytes, rather than stripped text\n    # of the premerge tests.\n    #\n    # For text items, we still have to decode('ascii','replace') because\n    # the tiff file format can't take 8 bit bytes in that field.\n\n    base_text_data = \"This is some arbitrary metadata for a text field\"\n    bin_data = base_text_data.encode(\"ascii\") + b\" \\xff\"\n    text_data = base_text_data + \" \" + chr(255)\n    reloaded_text_data = base_text_data + \" ?\"\n    float_data = 12.345\n    double_data = 67.89\n    info = TiffImagePlugin.ImageFileDirectory()\n\n    ImageJMetaData = TAG_IDS[\"ImageJMetaData\"]\n    ImageJMetaDataByteCounts = TAG_IDS[\"ImageJMetaDataByteCounts\"]\n    ImageDescription = TAG_IDS[\"ImageDescription\"]\n\n    info[ImageJMetaDataByteCounts] = len(bin_data)\n    info[ImageJMetaData] = bin_data\n    info[TAG_IDS[\"RollAngle\"]] = float_data\n    info.tagtype[TAG_IDS[\"RollAngle\"]] = 11\n    info[TAG_IDS[\"YawAngle\"]] = double_data\n    info.tagtype[TAG_IDS[\"YawAngle\"]] = 12\n\n    info[ImageDescription] = text_data\n\n    f = str(tmp_path / \"temp.tif\")\n\n    img.save(f, tiffinfo=info)\n\n    with Image.open(f) as loaded:\n\n        assert loaded.tag[ImageJMetaDataByteCounts] == (len(bin_data),)\n        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (len(bin_data),)\n\n        assert loaded.tag[ImageJMetaData] == bin_data\n        assert loaded.tag_v2[ImageJMetaData] == bin_data\n\n        assert loaded.tag[ImageDescription] == (reloaded_text_data,)\n        assert loaded.tag_v2[ImageDescription] == reloaded_text_data\n\n        loaded_float = loaded.tag[TAG_IDS[\"RollAngle\"]][0]\n        assert round(abs(loaded_float - float_data), 5) == 0\n        loaded_double = loaded.tag[TAG_IDS[\"YawAngle\"]][0]\n        assert round(abs(loaded_double - double_data), 7) == 0\n\n    # check with 2 element ImageJMetaDataByteCounts, issue #2006\n\n    info[ImageJMetaDataByteCounts] = (8, len(bin_data) - 8)\n    img.save(f, tiffinfo=info)\n    with Image.open(f) as loaded:\n\n        assert loaded.tag[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)\n        assert loaded.tag_v2[ImageJMetaDataByteCounts] == (8, len(bin_data) - 8)\n\n\ndef test_read_metadata():\n    with Image.open(\"Tests/images/hopper_g4.tif\") as img:\n\n        assert {\n            \"YResolution\": IFDRational(4294967295, 113653537),\n            \"PlanarConfiguration\": 1,\n            \"BitsPerSample\": (1,),\n            \"ImageLength\": 128,\n            \"Compression\": 4,\n            \"FillOrder\": 1,\n            \"RowsPerStrip\": 128,\n            \"ResolutionUnit\": 3,\n            \"PhotometricInterpretation\": 0,\n            \"PageNumber\": (0, 1),\n            \"XResolution\": IFDRational(4294967295, 113653537),\n            \"ImageWidth\": 128,\n            \"Orientation\": 1,\n            \"StripByteCounts\": (1968,),\n            \"SamplesPerPixel\": 1,\n            \"StripOffsets\": (8,),\n        } == img.tag_v2.named()\n\n        assert {\n            \"YResolution\": ((4294967295, 113653537),),\n            \"PlanarConfiguration\": (1,),\n            \"BitsPerSample\": (1,),\n            \"ImageLength\": (128,),\n            \"Compression\": (4,),\n            \"FillOrder\": (1,),\n            \"RowsPerStrip\": (128,),\n            \"ResolutionUnit\": (3,),\n            \"PhotometricInterpretation\": (0,),\n            \"PageNumber\": (0, 1),\n            \"XResolution\": ((4294967295, 113653537),),\n            \"ImageWidth\": (128,),\n            \"Orientation\": (1,),\n            \"StripByteCounts\": (1968,),\n            \"SamplesPerPixel\": (1,),\n            \"StripOffsets\": (8,),\n        } == img.tag.named()\n\n\ndef test_write_metadata(tmp_path):\n    \"\"\"Test metadata writing through the python code\"\"\"\n    with Image.open(\"Tests/images/hopper.tif\") as img:\n        f = str(tmp_path / \"temp.tiff\")\n        img.save(f, tiffinfo=img.tag)\n\n        original = img.tag_v2.named()\n\n    with Image.open(f) as loaded:\n        reloaded = loaded.tag_v2.named()\n\n    ignored = [\"StripByteCounts\", \"RowsPerStrip\", \"PageNumber\", \"StripOffsets\"]\n\n    for tag, value in reloaded.items():\n        if tag in ignored:\n            continue\n        if isinstance(original[tag], tuple) and isinstance(\n            original[tag][0], IFDRational\n        ):\n            # Need to compare element by element in the tuple,\n            # not comparing tuples of object references\n            assert_deep_equal(\n                original[tag],\n                value,\n                f\"{tag} didn't roundtrip, {original[tag]}, {value}\",\n            )\n        else:\n            assert (\n                original[tag] == value\n            ), f\"{tag} didn't roundtrip, {original[tag]}, {value}\"\n\n    for tag, value in original.items():\n        if tag not in ignored:\n            assert value == reloaded[tag], f\"{tag} didn't roundtrip\"\n\n\ndef test_change_stripbytecounts_tag_type(tmp_path):\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.tif\") as im:\n        info = im.tag_v2\n\n        # Resize the image so that STRIPBYTECOUNTS will be larger than a SHORT\n        im = im.resize((500, 500))\n\n        # STRIPBYTECOUNTS can be a SHORT or a LONG\n        info.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] = TiffTags.SHORT\n\n        im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2.tagtype[TiffImagePlugin.STRIPBYTECOUNTS] == TiffTags.LONG\n\n\ndef test_no_duplicate_50741_tag():\n    assert TAG_IDS[\"MakerNoteSafety\"] == 50741\n    assert TAG_IDS[\"BestQualityScale\"] == 50780\n\n\ndef test_iptc(tmp_path):\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.Lab.tif\") as im:\n        im.save(out)\n\n\ndef test_writing_other_types_to_ascii(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[271]\n    assert tag.type == TiffTags.ASCII\n\n    out = str(tmp_path / \"temp.tiff\")\n    for (value, expected) in {b\"test\": \"test\", 1: \"1\"}.items():\n        info[271] = value\n\n        im.save(out, tiffinfo=info)\n\n        with Image.open(out) as reloaded:\n            assert reloaded.tag_v2[271] == expected\n\n\ndef test_writing_int_to_bytes(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    tag = TiffTags.TAGS_V2[700]\n    assert tag.type == TiffTags.BYTE\n\n    info[700] = 1\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info)\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[700] == b\"\\x01\"\n\n\ndef test_undefined_zero(tmp_path):\n    # Check that the tag has not been changed since this test was created\n    tag = TiffTags.TAGS_V2[45059]\n    assert tag.type == TiffTags.UNDEFINED\n    assert tag.length == 0\n\n    info = TiffImagePlugin.ImageFileDirectory(b\"II*\\x00\\x08\\x00\\x00\\x00\")\n    info[45059] = b\"test\"\n\n    # Assert that the tag value does not change by setting it to itself\n    original = info[45059]\n    info[45059] = info[45059]\n    assert info[45059] == original\n\n\ndef test_empty_metadata():\n    f = io.BytesIO(b\"II*\\x00\\x08\\x00\\x00\\x00\")\n    head = f.read(8)\n    info = TiffImagePlugin.ImageFileDirectory(head)\n    # Should not raise struct.error.\n    pytest.warns(UserWarning, info.load, f)\n\n\ndef test_iccprofile(tmp_path):\n    # https://github.com/python-pillow/Pillow/issues/1462\n    out = str(tmp_path / \"temp.tiff\")\n    with Image.open(\"Tests/images/hopper.iccprofile.tif\") as im:\n        im.save(out)\n\n    with Image.open(out) as reloaded:\n        assert not isinstance(im.info[\"icc_profile\"], tuple)\n        assert im.info[\"icc_profile\"] == reloaded.info[\"icc_profile\"]\n\n\ndef test_iccprofile_binary():\n    # https://github.com/python-pillow/Pillow/issues/1526\n    # We should be able to load this,\n    # but probably won't be able to save it.\n\n    with Image.open(\"Tests/images/hopper.iccprofile_binary.tif\") as im:\n        assert im.tag_v2.tagtype[34675] == 1\n        assert im.info[\"icc_profile\"]\n\n\ndef test_iccprofile_save_png(tmp_path):\n    with Image.open(\"Tests/images/hopper.iccprofile.tif\") as im:\n        outfile = str(tmp_path / \"temp.png\")\n        im.save(outfile)\n\n\ndef test_iccprofile_binary_save_png(tmp_path):\n    with Image.open(\"Tests/images/hopper.iccprofile_binary.tif\") as im:\n        outfile = str(tmp_path / \"temp.png\")\n        im.save(outfile)\n\n\ndef test_exif_div_zero(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n    info[41988] = TiffImagePlugin.IFDRational(0, 0)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert 0 == reloaded.tag_v2[41988].numerator\n        assert 0 == reloaded.tag_v2[41988].denominator\n\n\ndef test_ifd_unsigned_rational(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    max_long = 2**32 - 1\n\n    # 4 bytes unsigned long\n    numerator = max_long\n\n    info[41493] = TiffImagePlugin.IFDRational(numerator, 1)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert max_long == reloaded.tag_v2[41493].numerator\n        assert 1 == reloaded.tag_v2[41493].denominator\n\n    # out of bounds of 4 byte unsigned long\n    numerator = max_long + 1\n\n    info[41493] = TiffImagePlugin.IFDRational(numerator, 1)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert max_long == reloaded.tag_v2[41493].numerator\n        assert 1 == reloaded.tag_v2[41493].denominator\n\n\ndef test_ifd_signed_rational(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    # pair of 4 byte signed longs\n    numerator = 2**31 - 1\n    denominator = -(2**31)\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert numerator == reloaded.tag_v2[37380].numerator\n        assert denominator == reloaded.tag_v2[37380].denominator\n\n    numerator = -(2**31)\n    denominator = 2**31 - 1\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert numerator == reloaded.tag_v2[37380].numerator\n        assert denominator == reloaded.tag_v2[37380].denominator\n\n    # out of bounds of 4 byte signed long\n    numerator = -(2**31) - 1\n    denominator = 1\n\n    info[37380] = TiffImagePlugin.IFDRational(numerator, denominator)\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert 2**31 - 1 == reloaded.tag_v2[37380].numerator\n        assert -1 == reloaded.tag_v2[37380].denominator\n\n\ndef test_ifd_signed_long(tmp_path):\n    im = hopper()\n    info = TiffImagePlugin.ImageFileDirectory_v2()\n\n    info[37000] = -60000\n\n    out = str(tmp_path / \"temp.tiff\")\n    im.save(out, tiffinfo=info, compression=\"raw\")\n\n    with Image.open(out) as reloaded:\n        assert reloaded.tag_v2[37000] == -60000\n\n\ndef test_empty_values():\n    data = io.BytesIO(\n        b\"II*\\x00\\x08\\x00\\x00\\x00\\x03\\x00\\x1a\\x01\\x05\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x1b\\x01\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x98\\x82\\x02\\x00\\x07\\x00\\x00\\x002\\x00\\x00\\x00\\x00\\x00\\x00\\x00a \"\n        b\"text\\x00\\x00\"\n    )\n    head = data.read(8)\n    info = TiffImagePlugin.ImageFileDirectory_v2(head)\n    info.load(data)\n    # Should not raise ValueError.\n    info = dict(info)\n    assert 33432 in info\n\n\ndef test_photoshop_info(tmp_path):\n    with Image.open(\"Tests/images/issue_2278.tif\") as im:\n        assert len(im.tag_v2[34377]) == 70\n        assert isinstance(im.tag_v2[34377], bytes)\n        out = str(tmp_path / \"temp.tiff\")\n        im.save(out)\n    with Image.open(out) as reloaded:\n        assert len(reloaded.tag_v2[34377]) == 70\n        assert isinstance(reloaded.tag_v2[34377], bytes)\n\n\ndef test_too_many_entries():\n    ifd = TiffImagePlugin.ImageFileDirectory_v2()\n\n    #    277: (\"SamplesPerPixel\", SHORT, 1),\n    ifd._tagdata[277] = struct.pack(\"hh\", 4, 4)\n    ifd.tagtype[277] = TiffTags.SHORT\n\n    # Should not raise ValueError.\n    pytest.warns(UserWarning, lambda: ifd[277])\n\n\ndef test_tag_group_data():\n    base_ifd = TiffImagePlugin.ImageFileDirectory_v2()\n    interop_ifd = TiffImagePlugin.ImageFileDirectory_v2(group=40965)\n    for ifd in (base_ifd, interop_ifd):\n        ifd[2] = \"test\"\n        ifd[256] = 10\n\n    assert base_ifd.tagtype[256] == 4\n    assert interop_ifd.tagtype[256] != base_ifd.tagtype[256]\n\n    assert interop_ifd.tagtype[2] == 7\n    assert base_ifd.tagtype[2] != interop_ifd.tagtype[256]\n\n\ndef test_empty_subifd(tmp_path):\n    out = str(tmp_path / \"temp.jpg\")\n\n    im = hopper()\n    exif = im.getexif()\n    exif[TiffImagePlugin.EXIFIFD] = {}\n    im.save(out, exif=exif)\n\n    with Image.open(out) as reloaded:\n        exif = reloaded.getexif()\n        assert exif.get_ifd(TiffImagePlugin.EXIFIFD) == {}\n"
    ],
    "test_file_names": [
        "test_1.py"
    ],
    "setup_res_code": 0,
    "setup_res_stdout": "Starting Pillow installation attempts...\nVerifying Pillow installation...\n✅ Pillow installation successful!\nSuccessfully installed Pillow\n",
    "setup_res_stderr": "Using CPython 3.9.21\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 12 packages in 10ms\nInstalled 12 packages in 195ms\n + coverage==7.6.10\n + exceptiongroup==1.2.2\n + iniconfig==2.0.0\n + packaging==24.2\n + pluggy==1.5.0\n + pyqt5==5.15.11\n + pyqt5-qt5==5.15.16\n + pyqt5-sip==12.16.1\n + pytest==8.3.4\n + pytest-cov==6.0.0\n + setuptools==75.8.0\n + tomli==2.2.1\nResolved 1 package in 858ms\nPrepared 1 package in 32.77s\nInstalled 1 package in 1ms\n + pillow==9.4.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_6da4169f3724ffe20c72d8ef4a2e0dc21815b343)\n",
    "new_commit_res_code": 0,
    "new_commit_res_stdout": "\u001b[1m============================= test session starts ==============================\u001b[0m\nplatform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_6da4169f3724ffe20c72d8ef4a2e0dc21815b343\nconfigfile: setup.cfg\nplugins: cov-6.0.0\ncollected 23 items\n\nr2e_tests/test_1.py \u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m                              [100%]\u001b[0m\n\n==================================== PASSES ====================================\n\u001b[36m\u001b[1m=========================== short test summary info ============================\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_rt_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_read_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_write_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_change_stripbytecounts_tag_type\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_no_duplicate_50741_tag\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iptc\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_writing_other_types_to_ascii\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_writing_int_to_bytes\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_undefined_zero\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile_binary\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile_save_png\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile_binary_save_png\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_exif_div_zero\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_ifd_unsigned_rational\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_ifd_signed_rational\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_ifd_signed_long\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_values\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_photoshop_info\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_too_many_entries\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_tag_group_data\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_subifd\u001b[0m\n\u001b[32m============================== \u001b[32m\u001b[1m23 passed\u001b[0m\u001b[32m in 0.26s\u001b[0m\u001b[32m ==============================\u001b[0m\n",
    "new_commit_res_stderr": "",
    "old_commit_res_code": 1,
    "old_commit_res_stdout": "\u001b[1m============================= test session starts ==============================\u001b[0m\nplatform linux -- Python 3.9.21, pytest-8.3.4, pluggy-1.5.0\nrootdir: /home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_6da4169f3724ffe20c72d8ef4a2e0dc21815b343\nconfigfile: setup.cfg\nplugins: cov-6.0.0\ncollected 23 items\n\nr2e_tests/test_1.py \u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31mF\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[32m.\u001b[0m\u001b[31m                              [100%]\u001b[0m\n\n=================================== FAILURES ===================================\n\u001b[31m\u001b[1m______________________ test_writing_other_types_to_ascii _______________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-326/test_writing_other_types_to_as0')\n\n    def test_writing_other_types_to_ascii(tmp_path):\n        im = hopper()\n        info = TiffImagePlugin.ImageFileDirectory_v2()\n    \n        tag = TiffTags.TAGS_V2[271]\n        assert tag.type == TiffTags.ASCII\n    \n        out = str(tmp_path / \"temp.tiff\")\n        for (value, expected) in {b\"test\": \"test\", 1: \"1\"}.items():\n            info[271] = value\n    \n>           im.save(out, tiffinfo=info)\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:199: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\u001b[1m\u001b[31msrc/PIL/Image.py\u001b[0m:2353: in save\n    save_handler(self, fp, filename)\n\u001b[1m\u001b[31msrc/PIL/TiffImagePlugin.py\u001b[0m:1876: in _save\n    offset = ifd.save(fp)\n\u001b[1m\u001b[31msrc/PIL/TiffImagePlugin.py\u001b[0m:938: in save\n    result = self.tobytes(offset)\n\u001b[1m\u001b[31msrc/PIL/TiffImagePlugin.py\u001b[0m:883: in tobytes\n    data = self._write_dispatch[typ](self, *values)\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <PIL.TiffImagePlugin.ImageFileDirectory_v2 object at 0x7483958d6430>\nvalue = 1\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if not isinstance(value, bytes):\n>           value = value.encode(\"ascii\", \"replace\")\n\u001b[1m\u001b[31mE           AttributeError: 'int' object has no attribute 'encode'\u001b[0m\n\n\u001b[1m\u001b[31msrc/PIL/TiffImagePlugin.py\u001b[0m:736: AttributeError\n==================================== PASSES ====================================\n\u001b[36m\u001b[1m=========================== short test summary info ============================\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_rt_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_read_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_write_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_change_stripbytecounts_tag_type\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_no_duplicate_50741_tag\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iptc\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_writing_int_to_bytes\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_undefined_zero\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_metadata\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile_binary\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile_save_png\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_iccprofile_binary_save_png\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_exif_div_zero\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_ifd_unsigned_rational\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_ifd_signed_rational\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_ifd_signed_long\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_values\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_photoshop_info\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_too_many_entries\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_tag_group_data\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_empty_subifd\u001b[0m\n\u001b[31mFAILED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_writing_other_types_to_ascii\u001b[0m - AttributeError: 'int' object has no attribute 'encode'\n\u001b[31m========================= \u001b[31m\u001b[1m1 failed\u001b[0m, \u001b[32m22 passed\u001b[0m\u001b[31m in 0.30s\u001b[0m\u001b[31m =========================\u001b[0m\n",
    "old_commit_res_stderr": ""
}
Tests/test_file_tiff_metadata.py src/PIL/TiffImagePlugin.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Writing integer values as ASCII tags raises AttributeError

**Description**
When assigning an integer to an ASCII tag in TIFF metadata and attempting to save the image, an `AttributeError` is triggered because integers do not have an `encode` method. This issue prevents users from using integer values for ASCII tags, which can be necessary for certain metadata specifications.

**Example Code**
```python
from PIL import Image, TiffImagePlugin, TiffTags

# Create a new image
im = Image.new("RGB", (100, 100))
info = TiffImagePlugin.ImageFileDirectory_v2()

# Assign an integer to an ASCII tag
tag = TiffTags.TAGS_V2[271]  # Example tag for ImageDescription
info[271] = 123  # Integer value

# Attempt to save the image with the TIFF metadata
out = "temp.tiff"
im.save(out, tiffinfo=info)
```

**Expected Behavior**
The integer value should be automatically converted to a string and saved as an ASCII tag in the TIFF metadata without any errors.

**Actual Behavior**
An `AttributeError` is raised with the message: `'int' object has no attribute 'encode'`.

[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### src/PIL/TiffImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# TIFF file handling
#
# TIFF is a flexible, if somewhat aged, image file format originally
# defined by Aldus.  Although TIFF supports a wide variety of pixel
# layouts and compression methods, the name doesn't really stand for
# "thousands of incompatible file formats," it just feels that way.
#
# To read TIFF data from a stream, the stream must be seekable.  For
# progressive decoding, make sure to use TIFF files where the tag
# directory is placed first in the file.
#
# History:
# 1995-09-01 fl   Created
# 1996-05-04 fl   Handle JPEGTABLES tag
# 1996-05-18 fl   Fixed COLORMAP support
# 1997-01-05 fl   Fixed PREDICTOR support
# 1997-08-27 fl   Added support for rational tags (from Perry Stoll)
# 1998-01-10 fl   Fixed seek/tell (from Jan Blom)
# 1998-07-15 fl   Use private names for internal variables
# 1999-06-13 fl   Rewritten for PIL 1.0 (1.0)
# 2000-10-11 fl   Additional fixes for Python 2.0 (1.1)
# 2001-04-17 fl   Fixed rewind support (seek to frame 0) (1.2)
# 2001-05-12 fl   Added write support for more tags (from Greg Couch) (1.3)
# 2001-12-18 fl   Added workaround for broken Matrox library
# 2002-01-18 fl   Don't mess up if photometric tag is missing (D. Alan Stewart)
# 2003-05-19 fl   Check FILLORDER tag
# 2003-09-26 fl   Added RGBa support
# 2004-02-24 fl   Added DPI support; fixed rational write support
# 2005-02-07 fl   Added workaround for broken Corel Draw 10 files
# 2006-01-09 fl   Added support for float/double tags (from Russell Nelson)
#
# Copyright (c) 1997-2006 by Secret Labs AB.  All rights reserved.
# Copyright (c) 1995-1997 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import io
import itertools
import logging
import math
import os
import struct
import warnings
from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational

from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
from .TiffTags import TYPES

logger = logging.getLogger(__name__)

# Set these to true to force use of libtiff for reading or writing.
READ_LIBTIFF = False
WRITE_LIBTIFF = False
IFD_LEGACY_API = True
STRIP_SIZE = 65536

II = b"II"  # little-endian (Intel style)
MM = b"MM"  # big-endian (Motorola style)

#
# --------------------------------------------------------------------
# Read TIFF files

# a few tag names, just to make the code below a bit more readable
IMAGEWIDTH = 256
IMAGELENGTH = 257
BITSPERSAMPLE = 258
COMPRESSION = 259
PHOTOMETRIC_INTERPRETATION = 262
FILLORDER = 266
IMAGEDESCRIPTION = 270
STRIPOFFSETS = 273
SAMPLESPERPIXEL = 277
ROWSPERSTRIP = 278
STRIPBYTECOUNTS = 279
X_RESOLUTION = 282
Y_RESOLUTION = 283
PLANAR_CONFIGURATION = 284
RESOLUTION_UNIT = 296
TRANSFERFUNCTION = 301
SOFTWARE = 305
DATE_TIME = 306
ARTIST = 315
PREDICTOR = 317
COLORMAP = 320
TILEWIDTH = 322
TILELENGTH = 323
TILEOFFSETS = 324
TILEBYTECOUNTS = 325
SUBIFD = 330
EXTRASAMPLES = 338
SAMPLEFORMAT = 339
JPEGTABLES = 347
YCBCRSUBSAMPLING = 530
REFERENCEBLACKWHITE = 532
COPYRIGHT = 33432
IPTC_NAA_CHUNK = 33723  # newsphoto properties
PHOTOSHOP_CHUNK = 34377  # photoshop properties
ICCPROFILE = 34675
EXIFIFD = 34665
XMP = 700
JPEGQUALITY = 65537  # pseudo-tag by libtiff

# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
IMAGEJ_META_DATA_BYTE_COUNTS = 50838
IMAGEJ_META_DATA = 50839

COMPRESSION_INFO = {
    # Compression => pil compression name
    1: "raw",
    2: "tiff_ccitt",
    3: "group3",
    4: "group4",
    5: "tiff_lzw",
    6: "tiff_jpeg",  # obsolete
    7: "jpeg",
    8: "tiff_adobe_deflate",
    32771: "tiff_raw_16",  # 16-bit padding
    32773: "packbits",
    32809: "tiff_thunderscan",
    32946: "tiff_deflate",
    34676: "tiff_sgilog",
    34677: "tiff_sgilog24",
    34925: "lzma",
    50000: "zstd",
    50001: "webp",
}

COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}

OPEN_INFO = {
    # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
    #  ExtraSamples) => mode, rawmode
    (II, 0, (1,), 1, (1,), ()): ("1", "1;I"),
    (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"),
    (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
    (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
    (II, 1, (1,), 1, (1,), ()): ("1", "1"),
    (MM, 1, (1,), 1, (1,), ()): ("1", "1"),
    (II, 1, (1,), 2, (1,), ()): ("1", "1;R"),
    (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"),
    (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
    (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
    (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
    (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
    (II, 1, (1,), 1, (2,), ()): ("L", "L;2"),
    (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"),
    (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
    (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
    (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
    (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
    (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
    (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
    (II, 1, (1,), 1, (4,), ()): ("L", "L;4"),
    (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"),
    (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
    (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
    (II, 0, (1,), 1, (8,), ()): ("L", "L;I"),
    (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"),
    (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
    (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
    (II, 1, (1,), 1, (8,), ()): ("L", "L"),
    (MM, 1, (1,), 1, (8,), ()): ("L", "L"),
    (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
    (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
    (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
    (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
    (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
    (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
    (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
    (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
    (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
    (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
    (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"),
    (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"),
    (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"),
    (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"),
    (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"),
    (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"),
    (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
    (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
    (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
    (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
    (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
    (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
    (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"),  # missing ExtraSamples
    (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"),  # missing ExtraSamples
    (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
    (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
    (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"),  # Corel Draw 10
    (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"),  # Corel Draw 10
    (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
    (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
    (II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
    (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"),
    (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
    (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
    (II, 3, (1,), 1, (2,), ()): ("P", "P;2"),
    (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"),
    (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
    (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
    (II, 3, (1,), 1, (4,), ()): ("P", "P;4"),
    (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"),
    (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
    (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
    (II, 3, (1,), 1, (8,), ()): ("P", "P"),
    (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
    (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
    (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
    (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
    (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"),
    (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
    (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
    (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
    (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
    (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
    (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
    (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
    # JPEG compressed images handled by LibTiff and auto-converted to RGBX
    # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
    (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
    (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
    (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
    (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
}

MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())

PREFIXES = [
    b"MM\x00\x2A",  # Valid TIFF header with big-endian byte order
    b"II\x2A\x00",  # Valid TIFF header with little-endian byte order
    b"MM\x2A\x00",  # Invalid TIFF header, assume big-endian
    b"II\x00\x2A",  # Invalid TIFF header, assume little-endian
    b"MM\x00\x2B",  # BigTIFF with big-endian byte order
    b"II\x2B\x00",  # BigTIFF with little-endian byte order
]


def _accept(prefix):
    return prefix[:4] in PREFIXES


def _limit_rational(val, max_val):
    inv = abs(val) > 1
    n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
    return n_d[::-1] if inv else n_d


def _limit_signed_rational(val, max_val, min_val):
    frac = Fraction(val)
    n_d = frac.numerator, frac.denominator

    if min(n_d) < min_val:
        n_d = _limit_rational(val, abs(min_val))

    if max(n_d) > max_val:
        val = Fraction(*n_d)
        n_d = _limit_rational(val, max_val)

    return n_d


##
# Wrapper for TIFF IFDs.

_load_dispatch = {}
_write_dispatch = {}


class IFDRational(Rational):
    """Implements a rational class where 0/0 is a legal value to match
    the in the wild use of exif rationals.

    e.g., DigitalZoomRatio - 0.00/0.00  indicates that no digital zoom was used
    """

    """ If the denominator is 0, store this as a float('nan'), otherwise store
    as a fractions.Fraction(). Delegate as appropriate

    """

    __slots__ = ("_numerator", "_denominator", "_val")

    def __init__(self, value, denominator=1):
        """
        :param value: either an integer numerator, a
        float/rational/other number, or an IFDRational
        :param denominator: Optional integer denominator
        """
        if isinstance(value, IFDRational):
            self._numerator = value.numerator
            self._denominator = value.denominator
            self._val = value._val
            return

        if isinstance(value, Fraction):
            self._numerator = value.numerator
            self._denominator = value.denominator
        else:
            self._numerator = value
            self._denominator = denominator

        if denominator == 0:
            self._val = float("nan")
        elif denominator == 1:
            self._val = Fraction(value)
        else:
            self._val = Fraction(value, denominator)

    @property
    def numerator(self):
        return self._numerator

    @property
    def denominator(self):
        return self._denominator

    def limit_rational(self, max_denominator):
        """

        :param max_denominator: Integer, the maximum denominator value
        :returns: Tuple of (numerator, denominator)
        """

        if self.denominator == 0:
            return self.numerator, self.denominator

        f = self._val.limit_denominator(max_denominator)
        return f.numerator, f.denominator

    def __repr__(self):
        return str(float(self._val))

    def __hash__(self):
        return self._val.__hash__()

    def __eq__(self, other):
        val = self._val
        if isinstance(other, IFDRational):
            other = other._val
        if isinstance(other, float):
            val = float(val)
        return val == other

    def __getstate__(self):
        return [self._val, self._numerator, self._denominator]

    def __setstate__(self, state):
        IFDRational.__init__(self, 0)
        _val, _numerator, _denominator = state
        self._val = _val
        self._numerator = _numerator
        self._denominator = _denominator

    def _delegate(op):
        def delegate(self, *args):
            return getattr(self._val, op)(*args)

        return delegate

    """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
             'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
             'mod','rmod', 'pow','rpow', 'pos', 'neg',
             'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',
             'ceil', 'floor', 'round']
        print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a))
        """

    __add__ = _delegate("__add__")
    __radd__ = _delegate("__radd__")
    __sub__ = _delegate("__sub__")
    __rsub__ = _delegate("__rsub__")
    __mul__ = _delegate("__mul__")
    __rmul__ = _delegate("__rmul__")
    __truediv__ = _delegate("__truediv__")
    __rtruediv__ = _delegate("__rtruediv__")
    __floordiv__ = _delegate("__floordiv__")
    __rfloordiv__ = _delegate("__rfloordiv__")
    __mod__ = _delegate("__mod__")
    __rmod__ = _delegate("__rmod__")
    __pow__ = _delegate("__pow__")
    __rpow__ = _delegate("__rpow__")
    __pos__ = _delegate("__pos__")
    __neg__ = _delegate("__neg__")
    __abs__ = _delegate("__abs__")
    __trunc__ = _delegate("__trunc__")
    __lt__ = _delegate("__lt__")
    __gt__ = _delegate("__gt__")
    __le__ = _delegate("__le__")
    __ge__ = _delegate("__ge__")
    __bool__ = _delegate("__bool__")
    __ceil__ = _delegate("__ceil__")
    __floor__ = _delegate("__floor__")
    __round__ = _delegate("__round__")


class ImageFileDirectory_v2(MutableMapping):
    """This class represents a TIFF tag directory.  To speed things up, we
    don't decode tags unless they're asked for.

    Exposes a dictionary interface of the tags in the directory::

        ifd = ImageFileDirectory_v2()
        ifd[key] = 'Some Data'
        ifd.tagtype[key] = TiffTags.ASCII
        print(ifd[key])
        'Some Data'

    Individual values are returned as the strings or numbers, sequences are
    returned as tuples of the values.

    The tiff metadata type of each item is stored in a dictionary of
    tag types in
    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
    are read from a tiff file, guessed from the type added, or added
    manually.

    Data Structures:

        * ``self.tagtype = {}``

          * Key: numerical TIFF tag number
          * Value: integer corresponding to the data type from
            :py:data:`.TiffTags.TYPES`

          .. versionadded:: 3.0.0

    'Internal' data structures:

        * ``self._tags_v2 = {}``

          * Key: numerical TIFF tag number
          * Value: decoded data, as tuple for multiple values

        * ``self._tagdata = {}``

          * Key: numerical TIFF tag number
          * Value: undecoded byte string from file

        * ``self._tags_v1 = {}``

          * Key: numerical TIFF tag number
          * Value: decoded data in the v1 format

    Tags will be found in the private attributes ``self._tagdata``, and in
    ``self._tags_v2`` once decoded.

    ``self.legacy_api`` is a value for internal use, and shouldn't be changed
    from outside code. In cooperation with
    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
    is true, then decoded tags will be populated into both ``_tags_v1`` and
    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
    save routine. Tags should be read from ``_tags_v1`` if
    ``legacy_api == true``.

    """

    def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
        """Initialize an ImageFileDirectory.

        To construct an ImageFileDirectory from a real file, pass the 8-byte
        magic header to the constructor.  To only set the endianness, pass it
        as the 'prefix' keyword argument.

        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
              endianness.
        :param prefix: Override the endianness of the file.
        """
        if not _accept(ifh):
            raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
        self._prefix = prefix if prefix is not None else ifh[:2]
        if self._prefix == MM:
            self._endian = ">"
        elif self._prefix == II:
            self._endian = "<"
        else:
            raise SyntaxError("not a TIFF IFD")
        self._bigtiff = ifh[2] == 43
        self.group = group
        self.tagtype = {}
        """ Dictionary of tag types """
        self.reset()
        (self.next,) = (
            self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:])
        )
        self._legacy_api = False

    prefix = property(lambda self: self._prefix)
    offset = property(lambda self: self._offset)
    legacy_api = property(lambda self: self._legacy_api)

    @legacy_api.setter
    def legacy_api(self, value):
        raise Exception("Not allowing setting of legacy api")

    def reset(self):
        self._tags_v1 = {}  # will remain empty if legacy_api is false
        self._tags_v2 = {}  # main tag storage
        self._tagdata = {}
        self.tagtype = {}  # added 2008-06-05 by Florian Hoech
        self._next = None
        self._offset = None

    def __str__(self):
        return str(dict(self))

    def named(self):
        """
        :returns: dict of name|key: value

        Returns the complete tag dictionary, with named tags where possible.
        """
        return {
            TiffTags.lookup(code, self.group).name: value
            for code, value in self.items()
        }

    def __len__(self):
        return len(set(self._tagdata) | set(self._tags_v2))

    def __getitem__(self, tag):
        if tag not in self._tags_v2:  # unpack on the fly
            data = self._tagdata[tag]
            typ = self.tagtype[tag]
            size, handler = self._load_dispatch[typ]
            self[tag] = handler(self, data, self.legacy_api)  # check type
        val = self._tags_v2[tag]
        if self.legacy_api and not isinstance(val, (tuple, bytes)):
            val = (val,)
        return val

    def __contains__(self, tag):
        return tag in self._tags_v2 or tag in self._tagdata

    def __setitem__(self, tag, value):
        self._setitem(tag, value, self.legacy_api)

    def _setitem(self, tag, value, legacy_api):
        basetypes = (Number, bytes, str)

        info = TiffTags.lookup(tag, self.group)
        values = [value] if isinstance(value, basetypes) else value

        if tag not in self.tagtype:
            if info.type:
                self.tagtype[tag] = info.type
            else:
                self.tagtype[tag] = TiffTags.UNDEFINED
                if all(isinstance(v, IFDRational) for v in values):
                    self.tagtype[tag] = (
                        TiffTags.RATIONAL
                        if all(v >= 0 for v in values)
                        else TiffTags.SIGNED_RATIONAL
                    )
                elif all(isinstance(v, int) for v in values):
                    if all(0 <= v < 2**16 for v in values):
                        self.tagtype[tag] = TiffTags.SHORT
                    elif all(-(2**15) < v < 2**15 for v in values):
                        self.tagtype[tag] = TiffTags.SIGNED_SHORT
                    else:
                        self.tagtype[tag] = (
                            TiffTags.LONG
                            if all(v >= 0 for v in values)
                            else TiffTags.SIGNED_LONG
                        )
                elif all(isinstance(v, float) for v in values):
                    self.tagtype[tag] = TiffTags.DOUBLE
                elif all(isinstance(v, str) for v in values):
                    self.tagtype[tag] = TiffTags.ASCII
                elif all(isinstance(v, bytes) for v in values):
                    self.tagtype[tag] = TiffTags.BYTE

        if self.tagtype[tag] == TiffTags.UNDEFINED:
            values = [
                v.encode("ascii", "replace") if isinstance(v, str) else v
                for v in values
            ]
        elif self.tagtype[tag] == TiffTags.RATIONAL:
            values = [float(v) if isinstance(v, int) else v for v in values]

        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
        if not is_ifd:
            values = tuple(info.cvt_enum(value) for value in values)

        dest = self._tags_v1 if legacy_api else self._tags_v2

        # Three branches:
        # Spec'd length == 1, Actual length 1, store as element
        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
        # Don't mess with the legacy api, since it's frozen.
        if not is_ifd and (
            (info.length == 1)
            or self.tagtype[tag] == TiffTags.BYTE
            or (info.length is None and len(values) == 1 and not legacy_api)
        ):
            # Don't mess with the legacy api, since it's frozen.
            if legacy_api and self.tagtype[tag] in [
                TiffTags.RATIONAL,
                TiffTags.SIGNED_RATIONAL,
            ]:  # rationals
                values = (values,)
            try:
                (dest[tag],) = values
            except ValueError:
                # We've got a builtin tag with 1 expected entry
                warnings.warn(
                    f"Metadata Warning, tag {tag} had too many entries: "
                    f"{len(values)}, expected 1"
                )
                dest[tag] = values[0]

        else:
            # Spec'd length > 1 or undefined
            # Unspec'd, and length > 1
            dest[tag] = values

    def __delitem__(self, tag):
        self._tags_v2.pop(tag, None)
        self._tags_v1.pop(tag, None)
        self._tagdata.pop(tag, None)

    def __iter__(self):
        return iter(set(self._tagdata) | set(self._tags_v2))

    def _unpack(self, fmt, data):
        return struct.unpack(self._endian + fmt, data)

    def _pack(self, fmt, *values):
        return struct.pack(self._endian + fmt, *values)

    def _register_loader(idx, size):
        def decorator(func):
            from .TiffTags import TYPES

            if func.__name__.startswith("load_"):
                TYPES[idx] = func.__name__[5:].replace("_", " ")
            _load_dispatch[idx] = size, func  # noqa: F821
            return func

        return decorator

    def _register_writer(idx):
        def decorator(func):
            _write_dispatch[idx] = func  # noqa: F821
            return func

        return decorator

    def _register_basic(idx_fmt_name):
        from .TiffTags import TYPES

        idx, fmt, name = idx_fmt_name
        TYPES[idx] = name
        size = struct.calcsize("=" + fmt)
        _load_dispatch[idx] = (  # noqa: F821
            size,
            lambda self, data, legacy_api=True: (
                self._unpack(f"{len(data) // size}{fmt}", data)
            ),
        )
        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821
            b"".join(self._pack(fmt, value) for value in values)
        )

    list(
        map(
            _register_basic,
            [
                (TiffTags.SHORT, "H", "short"),
                (TiffTags.LONG, "L", "long"),
                (TiffTags.SIGNED_BYTE, "b", "signed byte"),
                (TiffTags.SIGNED_SHORT, "h", "signed short"),
                (TiffTags.SIGNED_LONG, "l", "signed long"),
                (TiffTags.FLOAT, "f", "float"),
                (TiffTags.DOUBLE, "d", "double"),
                (TiffTags.IFD, "L", "long"),
                (TiffTags.LONG8, "Q", "long8"),
            ],
        )
    )

    @_register_loader(1, 1)  # Basic type, except for the legacy API.
    def load_byte(self, data, legacy_api=True):
        return data

    @_register_writer(1)  # Basic type, except for the legacy API.
    def write_byte(self, data):
        if isinstance(data, int):
            data = bytes((data,))
        return data

    @_register_loader(2, 1)
    def load_string(self, data, legacy_api=True):
        if data.endswith(b"\0"):
            data = data[:-1]
        return data.decode("latin-1", "replace")

    @_register_writer(2)
    def write_string(self, value):
        # remerge of https://github.com/python-pillow/Pillow/pull/1416
        if not isinstance(value, bytes):
            value = value.encode("ascii", "replace")
        return value + b"\0"

    @_register_loader(5, 8)
    def load_rational(self, data, legacy_api=True):
        vals = self._unpack(f"{len(data) // 4}L", data)

        def combine(a, b):
            return (a, b) if legacy_api else IFDRational(a, b)

        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))

    @_register_writer(5)
    def write_rational(self, *values):
        return b"".join(
            self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
        )

    @_register_loader(7, 1)
    def load_undefined(self, data, legacy_api=True):
        return data

    @_register_writer(7)
    def write_undefined(self, value):
        return value

    @_register_loader(10, 8)
    def load_signed_rational(self, data, legacy_api=True):
        vals = self._unpack(f"{len(data) // 4}l", data)

        def combine(a, b):
            return (a, b) if legacy_api else IFDRational(a, b)

        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))

    @_register_writer(10)
    def write_signed_rational(self, *values):
        return b"".join(
            self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
            for frac in values
        )

    def _ensure_read(self, fp, size):
        ret = fp.read(size)
        if len(ret) != size:
            raise OSError(
                "Corrupt EXIF data.  "
                f"Expecting to read {size} bytes but only got {len(ret)}. "
            )
        return ret

    def load(self, fp):

        self.reset()
        self._offset = fp.tell()

        try:
            tag_count = (
                self._unpack("Q", self._ensure_read(fp, 8))
                if self._bigtiff
                else self._unpack("H", self._ensure_read(fp, 2))
            )[0]
            for i in range(tag_count):
                tag, typ, count, data = (
                    self._unpack("HHQ8s", self._ensure_read(fp, 20))
                    if self._bigtiff
                    else self._unpack("HHL4s", self._ensure_read(fp, 12))
                )

                tagname = TiffTags.lookup(tag, self.group).name
                typname = TYPES.get(typ, "unknown")
                msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"

                try:
                    unit_size, handler = self._load_dispatch[typ]
                except KeyError:
                    logger.debug(msg + f" - unsupported type {typ}")
                    continue  # ignore unsupported type
                size = count * unit_size
                if size > (8 if self._bigtiff else 4):
                    here = fp.tell()
                    (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
                    msg += f" Tag Location: {here} - Data Location: {offset}"
                    fp.seek(offset)
                    data = ImageFile._safe_read(fp, size)
                    fp.seek(here)
                else:
                    data = data[:size]

                if len(data) != size:
                    warnings.warn(
                        "Possibly corrupt EXIF data.  "
                        f"Expecting to read {size} bytes but only got {len(data)}."
                        f" Skipping tag {tag}"
                    )
                    logger.debug(msg)
                    continue

                if not data:
                    logger.debug(msg)
                    continue

                self._tagdata[tag] = data
                self.tagtype[tag] = typ

                msg += " - value: " + (
                    "<table: %d bytes>" % size if size > 32 else repr(data)
                )
                logger.debug(msg)

            (self.next,) = (
                self._unpack("Q", self._ensure_read(fp, 8))
                if self._bigtiff
                else self._unpack("L", self._ensure_read(fp, 4))
            )
        except OSError as msg:
            warnings.warn(str(msg))
            return

    def tobytes(self, offset=0):
        # FIXME What about tagdata?
        result = self._pack("H", len(self._tags_v2))

        entries = []
        offset = offset + len(result) + len(self._tags_v2) * 12 + 4
        stripoffsets = None

        # pass 1: convert tags to binary format
        # always write tags in ascending order
        for tag, value in sorted(self._tags_v2.items()):
            if tag == STRIPOFFSETS:
                stripoffsets = len(entries)
            typ = self.tagtype.get(tag)
            logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}")
            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
            if is_ifd:
                if self._endian == "<":
                    ifh = b"II\x2A\x00\x08\x00\x00\x00"
                else:
                    ifh = b"MM\x00\x2A\x00\x00\x00\x08"
                ifd = ImageFileDirectory_v2(ifh, group=tag)
                values = self._tags_v2[tag]
                for ifd_tag, ifd_value in values.items():
                    ifd[ifd_tag] = ifd_value
                data = ifd.tobytes(offset)
            else:
                values = value if isinstance(value, tuple) else (value,)
                data = self._write_dispatch[typ](self, *values)

            tagname = TiffTags.lookup(tag, self.group).name
            typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
            msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})"
            msg += " - value: " + (
                "<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
            )
            logger.debug(msg)

            # count is sum of lengths for string and arbitrary data
            if is_ifd:
                count = 1
            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
                count = len(data)
            else:
                count = len(values)
            # figure out if data fits into the entry
            if len(data) <= 4:
                entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
            else:
                entries.append((tag, typ, count, self._pack("L", offset), data))
                offset += (len(data) + 1) // 2 * 2  # pad to word

        # update strip offset data to point beyond auxiliary data
        if stripoffsets is not None:
            tag, typ, count, value, data = entries[stripoffsets]
            if data:
                raise NotImplementedError("multistrip support not yet implemented")
            value = self._pack("L", self._unpack("L", value)[0] + offset)
            entries[stripoffsets] = tag, typ, count, value, data

        # pass 2: write entries to file
        for tag, typ, count, value, data in entries:
            logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}")
            result += self._pack("HHL4s", tag, typ, count, value)

        # -- overwrite here for multi-page --
        result += b"\0\0\0\0"  # end of entries

        # pass 3: write auxiliary data to file
        for tag, typ, count, value, data in entries:
            result += data
            if len(data) & 1:
                result += b"\0"

        return result

    def save(self, fp):

        if fp.tell() == 0:  # skip TIFF header on subsequent pages
            # tiff header -- PIL always starts the first IFD at offset 8
            fp.write(self._prefix + self._pack("HL", 42, 8))

        offset = fp.tell()
        result = self.tobytes(offset)
        fp.write(result)
        return offset + len(result)


ImageFileDirectory_v2._load_dispatch = _load_dispatch
ImageFileDirectory_v2._write_dispatch = _write_dispatch
for idx, name in TYPES.items():
    name = name.replace(" ", "_")
    setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1])
    setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx])
del _load_dispatch, _write_dispatch, idx, name


# Legacy ImageFileDirectory support.
class ImageFileDirectory_v1(ImageFileDirectory_v2):
    """This class represents the **legacy** interface to a TIFF tag directory.

    Exposes a dictionary interface of the tags in the directory::

        ifd = ImageFileDirectory_v1()
        ifd[key] = 'Some Data'
        ifd.tagtype[key] = TiffTags.ASCII
        print(ifd[key])
        ('Some Data',)

    Also contains a dictionary of tag types as read from the tiff image file,
    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.

    Values are returned as a tuple.

    ..  deprecated:: 3.0.0
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._legacy_api = True

    tags = property(lambda self: self._tags_v1)
    tagdata = property(lambda self: self._tagdata)

    # defined in ImageFileDirectory_v2
    tagtype: dict
    """Dictionary of tag types"""

    @classmethod
    def from_v2(cls, original):
        """Returns an
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
        instance with the same data as is contained in the original
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
        instance.

        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`

        """

        ifd = cls(prefix=original.prefix)
        ifd._tagdata = original._tagdata
        ifd.tagtype = original.tagtype
        ifd.next = original.next  # an indicator for multipage tiffs
        return ifd

    def to_v2(self):
        """Returns an
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
        instance with the same data as is contained in the original
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
        instance.

        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`

        """

        ifd = ImageFileDirectory_v2(prefix=self.prefix)
        ifd._tagdata = dict(self._tagdata)
        ifd.tagtype = dict(self.tagtype)
        ifd._tags_v2 = dict(self._tags_v2)
        return ifd

    def __contains__(self, tag):
        return tag in self._tags_v1 or tag in self._tagdata

    def __len__(self):
        return len(set(self._tagdata) | set(self._tags_v1))

    def __iter__(self):
        return iter(set(self._tagdata) | set(self._tags_v1))

    def __setitem__(self, tag, value):
        for legacy_api in (False, True):
            self._setitem(tag, value, legacy_api)

    def __getitem__(self, tag):
        if tag not in self._tags_v1:  # unpack on the fly
            data = self._tagdata[tag]
            typ = self.tagtype[tag]
            size, handler = self._load_dispatch[typ]
            for legacy in (False, True):
                self._setitem(tag, handler(self, data, legacy), legacy)
        val = self._tags_v1[tag]
        if not isinstance(val, (tuple, bytes)):
            val = (val,)
        return val


# undone -- switch this pointer when IFD_LEGACY_API == False
ImageFileDirectory = ImageFileDirectory_v1


##
# Image plugin for TIFF files.


class TiffImageFile(ImageFile.ImageFile):

    format = "TIFF"
    format_description = "Adobe TIFF"
    _close_exclusive_fp_after_loading = False

    def __init__(self, fp=None, filename=None):
        self.tag_v2 = None
        """ Image file directory (tag dictionary) """

        self.tag = None
        """ Legacy tag entries """

        super().__init__(fp, filename)

    def _open(self):
        """Open the first image in a TIFF file"""

        # Header
        ifh = self.fp.read(8)
        if ifh[2] == 43:
            ifh += self.fp.read(8)

        self.tag_v2 = ImageFileDirectory_v2(ifh)

        # legacy IFD entries will be filled in later
        self.ifd = None

        # setup frame pointers
        self.__first = self.__next = self.tag_v2.next
        self.__frame = -1
        self._fp = self.fp
        self._frame_pos = []
        self._n_frames = None

        logger.debug("*** TiffImageFile._open ***")
        logger.debug(f"- __first: {self.__first}")
        logger.debug(f"- ifh: {repr(ifh)}")  # Use repr to avoid str(bytes)

        # and load the first frame
        self._seek(0)

    @property
    def n_frames(self):
        if self._n_frames is None:
            current = self.tell()
            self._seek(len(self._frame_pos))
            while self._n_frames is None:
                self._seek(self.tell() + 1)
            self.seek(current)
        return self._n_frames

    def seek(self, frame):
        """Select a given frame as current image"""
        if not self._seek_check(frame):
            return
        self._seek(frame)
        # Create a new core image object on second and
        # subsequent frames in the image. Image may be
        # different size/mode.
        Image._decompression_bomb_check(self.size)
        self.im = Image.core.new(self.mode, self.size)

    def _seek(self, frame):
        self.fp = self._fp

        # reset buffered io handle in case fp
        # was passed to libtiff, invalidating the buffer
        self.fp.tell()

        while len(self._frame_pos) <= frame:
            if not self.__next:
                raise EOFError("no more images in TIFF file")
            logger.debug(
                f"Seeking to frame {frame}, on frame {self.__frame}, "
                f"__next {self.__next}, location: {self.fp.tell()}"
            )
            self.fp.seek(self.__next)
            self._frame_pos.append(self.__next)
            logger.debug("Loading tags, location: %s" % self.fp.tell())
            self.tag_v2.load(self.fp)
            if self.tag_v2.next in self._frame_pos:
                # This IFD has already been processed
                # Declare this to be the end of the image
                self.__next = 0
            else:
                self.__next = self.tag_v2.next
            if self.__next == 0:
                self._n_frames = frame + 1
            if len(self._frame_pos) == 1:
                self.is_animated = self.__next != 0
            self.__frame += 1
        self.fp.seek(self._frame_pos[frame])
        self.tag_v2.load(self.fp)
        self._reload_exif()
        # fill the legacy tag/ifd entries
        self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
        self.__frame = frame
        self._setup()

    def tell(self):
        """Return the current frame number"""
        return self.__frame

    def get_child_images(self):
        if SUBIFD not in self.tag_v2:
            return []
        child_images = []
        exif = self.getexif()
        offset = None
        for im_offset in self.tag_v2[SUBIFD]:
            # reset buffered io handle in case fp
            # was passed to libtiff, invalidating the buffer
            current_offset = self._fp.tell()
            if offset is None:
                offset = current_offset

            fp = self._fp
            ifd = exif._get_ifd_dict(im_offset)
            jpegInterchangeFormat = ifd.get(513)
            if jpegInterchangeFormat is not None:
                fp.seek(jpegInterchangeFormat)
                jpeg_data = fp.read(ifd.get(514))

                fp = io.BytesIO(jpeg_data)

            with Image.open(fp) as im:
                if jpegInterchangeFormat is None:
                    im._frame_pos = [im_offset]
                    im._seek(0)
                im.load()
                child_images.append(im)

        if offset is not None:
            self._fp.seek(offset)
        return child_images

    def getxmp(self):
        """
        Returns a dictionary containing the XMP tags.
        Requires defusedxml to be installed.

        :returns: XMP tags in a dictionary.
        """
        return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}

    def get_photoshop_blocks(self):
        """
        Returns a dictionary of Photoshop "Image Resource Blocks".
        The keys are the image resource ID. For more information, see
        https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727

        :returns: Photoshop "Image Resource Blocks" in a dictionary.
        """
        blocks = {}
        val = self.tag_v2.get(0x8649)
        if val:
            while val[:4] == b"8BIM":
                id = i16(val[4:6])
                n = math.ceil((val[6] + 1) / 2) * 2
                size = i32(val[6 + n : 10 + n])
                data = val[10 + n : 10 + n + size]
                blocks[id] = {"data": data}

                val = val[math.ceil((10 + n + size) / 2) * 2 :]
        return blocks

    def load(self):
        if self.tile and self.use_load_libtiff:
            return self._load_libtiff()
        return super().load()

    def load_end(self):
        if self._tile_orientation:
            method = {
                2: Image.Transpose.FLIP_LEFT_RIGHT,
                3: Image.Transpose.ROTATE_180,
                4: Image.Transpose.FLIP_TOP_BOTTOM,
                5: Image.Transpose.TRANSPOSE,
                6: Image.Transpose.ROTATE_270,
                7: Image.Transpose.TRANSVERSE,
                8: Image.Transpose.ROTATE_90,
            }.get(self._tile_orientation)
            if method is not None:
                self.im = self.im.transpose(method)
                self._size = self.im.size

        # allow closing if we're on the first frame, there's no next
        # This is the ImageFile.load path only, libtiff specific below.
        if not self.is_animated:
            self._close_exclusive_fp_after_loading = True

            # reset buffered io handle in case fp
            # was passed to libtiff, invalidating the buffer
            self.fp.tell()

            # load IFD data from fp before it is closed
            exif = self.getexif()
            for key in TiffTags.TAGS_V2_GROUPS.keys():
                if key not in exif:
                    continue
                exif.get_ifd(key)

    def _load_libtiff(self):
        """Overload method triggered when we detect a compressed tiff
        Calls out to libtiff"""

        Image.Image.load(self)

        self.load_prepare()

        if not len(self.tile) == 1:
            raise OSError("Not exactly one tile")

        # (self._compression, (extents tuple),
        #   0, (rawmode, self._compression, fp))
        extents = self.tile[0][1]
        args = list(self.tile[0][3])

        # To be nice on memory footprint, if there's a
        # file descriptor, use that instead of reading
        # into a string in python.
        # libtiff closes the file descriptor, so pass in a dup.
        try:
            fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno())
            # flush the file descriptor, prevents error on pypy 2.4+
            # should also eliminate the need for fp.tell
            # in _seek
            if hasattr(self.fp, "flush"):
                self.fp.flush()
        except OSError:
            # io.BytesIO have a fileno, but returns an OSError if
            # it doesn't use a file descriptor.
            fp = False

        if fp:
            args[2] = fp

        decoder = Image._getdecoder(
            self.mode, "libtiff", tuple(args), self.decoderconfig
        )
        try:
            decoder.setimage(self.im, extents)
        except ValueError as e:
            raise OSError("Couldn't set the image") from e

        close_self_fp = self._exclusive_fp and not self.is_animated
        if hasattr(self.fp, "getvalue"):
            # We've got a stringio like thing passed in. Yay for all in memory.
            # The decoder needs the entire file in one shot, so there's not
            # a lot we can do here other than give it the entire file.
            # unless we could do something like get the address of the
            # underlying string for stringio.
            #
            # Rearranging for supporting byteio items, since they have a fileno
            # that returns an OSError if there's no underlying fp. Easier to
            # deal with here by reordering.
            logger.debug("have getvalue. just sending in a string from getvalue")
            n, err = decoder.decode(self.fp.getvalue())
        elif fp:
            # we've got a actual file on disk, pass in the fp.
            logger.debug("have fileno, calling fileno version of the decoder.")
            if not close_self_fp:
                self.fp.seek(0)
            # 4 bytes, otherwise the trace might error out
            n, err = decoder.decode(b"fpfp")
        else:
            # we have something else.
            logger.debug("don't have fileno or getvalue. just reading")
            self.fp.seek(0)
            # UNDONE -- so much for that buffer size thing.
            n, err = decoder.decode(self.fp.read())

        if fp:
            try:
                os.close(fp)
            except OSError:
                pass

        self.tile = []
        self.readonly = 0

        self.load_end()

        # libtiff closed the fp in a, we need to close self.fp, if possible
        if close_self_fp:
            self.fp.close()
            self.fp = None  # might be shared

        if err < 0:
            raise OSError(err)

        return Image.Image.load(self)

    def _setup(self):
        """Setup this image object based on current tags"""

        if 0xBC01 in self.tag_v2:
            raise OSError("Windows Media Photo files not yet supported")

        # extract relevant tags
        self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
        self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)

        # photometric is a required tag, but not everyone is reading
        # the specification
        photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)

        # old style jpeg compression images most certainly are YCbCr
        if self._compression == "tiff_jpeg":
            photo = 6

        fillorder = self.tag_v2.get(FILLORDER, 1)

        logger.debug("*** Summary ***")
        logger.debug(f"- compression: {self._compression}")
        logger.debug(f"- photometric_interpretation: {photo}")
        logger.debug(f"- planar_configuration: {self._planar_configuration}")
        logger.debug(f"- fill_order: {fillorder}")
        logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")

        # size
        xsize = int(self.tag_v2.get(IMAGEWIDTH))
        ysize = int(self.tag_v2.get(IMAGELENGTH))
        self._size = xsize, ysize

        logger.debug(f"- size: {self.size}")

        sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
        if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
            # SAMPLEFORMAT is properly per band, so an RGB image will
            # be (1,1,1).  But, we don't support per band pixel types,
            # and anything more than one band is a uint8. So, just
            # take the first element. Revisit this if adding support
            # for more exotic images.
            sample_format = (1,)

        bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
        extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
        if photo in (2, 6, 8):  # RGB, YCbCr, LAB
            bps_count = 3
        elif photo == 5:  # CMYK
            bps_count = 4
        else:
            bps_count = 1
        bps_count += len(extra_tuple)
        bps_actual_count = len(bps_tuple)
        samples_per_pixel = self.tag_v2.get(
            SAMPLESPERPIXEL,
            3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
        )

        if samples_per_pixel > MAX_SAMPLESPERPIXEL:
            # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
            logger.error(
                "More samples per pixel than can be decoded: %s", samples_per_pixel
            )
            raise SyntaxError("Invalid value for samples per pixel")

        if samples_per_pixel < bps_actual_count:
            # If a file has more values in bps_tuple than expected,
            # remove the excess.
            bps_tuple = bps_tuple[:samples_per_pixel]
        elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:
            # If a file has only one value in bps_tuple, when it should have more,
            # presume it is the same number of bits for all of the samples.
            bps_tuple = bps_tuple * samples_per_pixel

        if len(bps_tuple) != samples_per_pixel:
            raise SyntaxError("unknown data organization")

        # mode: check photometric interpretation and bits per pixel
        key = (
            self.tag_v2.prefix,
            photo,
            sample_format,
            fillorder,
            bps_tuple,
            extra_tuple,
        )
        logger.debug(f"format key: {key}")
        try:
            self.mode, rawmode = OPEN_INFO[key]
        except KeyError as e:
            logger.debug("- unsupported format")
            raise SyntaxError("unknown pixel mode") from e

        logger.debug(f"- raw mode: {rawmode}")
        logger.debug(f"- pil mode: {self.mode}")

        self.info["compression"] = self._compression

        xres = self.tag_v2.get(X_RESOLUTION, 1)
        yres = self.tag_v2.get(Y_RESOLUTION, 1)

        if xres and yres:
            resunit = self.tag_v2.get(RESOLUTION_UNIT)
            if resunit == 2:  # dots per inch
                self.info["dpi"] = (xres, yres)
            elif resunit == 3:  # dots per centimeter. convert to dpi
                self.info["dpi"] = (xres * 2.54, yres * 2.54)
            elif resunit is None:  # used to default to 1, but now 2)
                self.info["dpi"] = (xres, yres)
                # For backward compatibility,
                # we also preserve the old behavior
                self.info["resolution"] = xres, yres
            else:  # No absolute unit of measurement
                self.info["resolution"] = xres, yres

        # build tile descriptors
        x = y = layer = 0
        self.tile = []
        self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
        if self.use_load_libtiff:
            # Decoder expects entire file as one tile.
            # There's a buffer size limit in load (64k)
            # so large g4 images will fail if we use that
            # function.
            #
            # Setup the one tile for the whole image, then
            # use the _load_libtiff function.

            # libtiff handles the fillmode for us, so 1;IR should
            # actually be 1;I. Including the R double reverses the
            # bits, so stripes of the image are reversed.  See
            # https://github.com/python-pillow/Pillow/issues/279
            if fillorder == 2:
                # Replace fillorder with fillorder=1
                key = key[:3] + (1,) + key[4:]
                logger.debug(f"format key: {key}")
                # this should always work, since all the
                # fillorder==2 modes have a corresponding
                # fillorder=1 mode
                self.mode, rawmode = OPEN_INFO[key]
            # libtiff always returns the bytes in native order.
            # we're expecting image byte order. So, if the rawmode
            # contains I;16, we need to convert from native to image
            # byte order.
            if rawmode == "I;16":
                rawmode = "I;16N"
            if ";16B" in rawmode:
                rawmode = rawmode.replace(";16B", ";16N")
            if ";16L" in rawmode:
                rawmode = rawmode.replace(";16L", ";16N")

            # YCbCr images with new jpeg compression with pixels in one plane
            # unpacked straight into RGB values
            if (
                photo == 6
                and self._compression == "jpeg"
                and self._planar_configuration == 1
            ):
                rawmode = "RGB"

            # Offset in the tile tuple is 0, we go from 0,0 to
            # w,h, and we only do this once -- eds
            a = (rawmode, self._compression, False, self.tag_v2.offset)
            self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a))

        elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
            # striped image
            if STRIPOFFSETS in self.tag_v2:
                offsets = self.tag_v2[STRIPOFFSETS]
                h = self.tag_v2.get(ROWSPERSTRIP, ysize)
                w = self.size[0]
            else:
                # tiled image
                offsets = self.tag_v2[TILEOFFSETS]
                w = self.tag_v2.get(TILEWIDTH)
                h = self.tag_v2.get(TILELENGTH)

            for offset in offsets:
                if x + w > xsize:
                    stride = w * sum(bps_tuple) / 8  # bytes per line
                else:
                    stride = 0

                tile_rawmode = rawmode
                if self._planar_configuration == 2:
                    # each band on it's own layer
                    tile_rawmode = rawmode[layer]
                    # adjust stride width accordingly
                    stride /= bps_count

                a = (tile_rawmode, int(stride), 1)
                self.tile.append(
                    (
                        self._compression,
                        (x, y, min(x + w, xsize), min(y + h, ysize)),
                        offset,
                        a,
                    )
                )
                x = x + w
                if x >= self.size[0]:
                    x, y = 0, y + h
                    if y >= self.size[1]:
                        x = y = 0
                        layer += 1
        else:
            logger.debug("- unsupported data organization")
            raise SyntaxError("unknown data organization")

        # Fix up info.
        if ICCPROFILE in self.tag_v2:
            self.info["icc_profile"] = self.tag_v2[ICCPROFILE]

        # fixup palette descriptor

        if self.mode in ["P", "PA"]:
            palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
            self.palette = ImagePalette.raw("RGB;L", b"".join(palette))

        self._tile_orientation = self.tag_v2.get(0x0112)


#
# --------------------------------------------------------------------
# Write TIFF files

# little endian is default except for image modes with
# explicit big endian byte-order

SAVE_INFO = {
    # mode => rawmode, byteorder, photometrics,
    #           sampleformat, bitspersample, extra
    "1": ("1", II, 1, 1, (1,), None),
    "L": ("L", II, 1, 1, (8,), None),
    "LA": ("LA", II, 1, 1, (8, 8), 2),
    "P": ("P", II, 3, 1, (8,), None),
    "PA": ("PA", II, 3, 1, (8, 8), 2),
    "I": ("I;32S", II, 1, 2, (32,), None),
    "I;16": ("I;16", II, 1, 1, (16,), None),
    "I;16S": ("I;16S", II, 1, 2, (16,), None),
    "F": ("F;32F", II, 1, 3, (32,), None),
    "RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
    "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
    "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
    "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
    "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
    "LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
    "I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
    "I;16B": ("I;16B", MM, 1, 1, (16,), None),
    "I;16BS": ("I;16BS", MM, 1, 2, (16,), None),
    "F;32BF": ("F;32BF", MM, 1, 3, (32,), None),
}


def _save(im, fp, filename):

    try:
        rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
    except KeyError as e:
        raise OSError(f"cannot write mode {im.mode} as TIFF") from e

    ifd = ImageFileDirectory_v2(prefix=prefix)

    encoderinfo = im.encoderinfo
    encoderconfig = im.encoderconfig
    try:
        compression = encoderinfo["compression"]
    except KeyError:
        compression = im.info.get("compression")
        if isinstance(compression, int):
            # compression value may be from BMP. Ignore it
            compression = None
    if compression is None:
        compression = "raw"
    elif compression == "tiff_jpeg":
        # OJPEG is obsolete, so use new-style JPEG compression instead
        compression = "jpeg"
    elif compression == "tiff_deflate":
        compression = "tiff_adobe_deflate"

    libtiff = WRITE_LIBTIFF or compression != "raw"

    # required for color libtiff images
    ifd[PLANAR_CONFIGURATION] = 1

    ifd[IMAGEWIDTH] = im.size[0]
    ifd[IMAGELENGTH] = im.size[1]

    # write any arbitrary tags passed in as an ImageFileDirectory
    if "tiffinfo" in encoderinfo:
        info = encoderinfo["tiffinfo"]
    elif "exif" in encoderinfo:
        info = encoderinfo["exif"]
        if isinstance(info, bytes):
            exif = Image.Exif()
            exif.load(info)
            info = exif
    else:
        info = {}
    logger.debug("Tiffinfo Keys: %s" % list(info))
    if isinstance(info, ImageFileDirectory_v1):
        info = info.to_v2()
    for key in info:
        if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():
            ifd[key] = info.get_ifd(key)
        else:
            ifd[key] = info.get(key)
        try:
            ifd.tagtype[key] = info.tagtype[key]
        except Exception:
            pass  # might not be an IFD. Might not have populated type

    # additions written by Greg Couch, gregc@cgl.ucsf.edu
    # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
    if hasattr(im, "tag_v2"):
        # preserve tags from original TIFF image file
        for key in (
            RESOLUTION_UNIT,
            X_RESOLUTION,
            Y_RESOLUTION,
            IPTC_NAA_CHUNK,
            PHOTOSHOP_CHUNK,
            XMP,
        ):
            if key in im.tag_v2:
                ifd[key] = im.tag_v2[key]
                ifd.tagtype[key] = im.tag_v2.tagtype[key]

    # preserve ICC profile (should also work when saving other formats
    # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
    icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
    if icc:
        ifd[ICCPROFILE] = icc

    for key, name in [
        (IMAGEDESCRIPTION, "description"),
        (X_RESOLUTION, "resolution"),
        (Y_RESOLUTION, "resolution"),
        (X_RESOLUTION, "x_resolution"),
        (Y_RESOLUTION, "y_resolution"),
        (RESOLUTION_UNIT, "resolution_unit"),
        (SOFTWARE, "software"),
        (DATE_TIME, "date_time"),
        (ARTIST, "artist"),
        (COPYRIGHT, "copyright"),
    ]:
        if name in encoderinfo:
            ifd[key] = encoderinfo[name]

    dpi = encoderinfo.get("dpi")
    if dpi:
        ifd[RESOLUTION_UNIT] = 2
        ifd[X_RESOLUTION] = dpi[0]
        ifd[Y_RESOLUTION] = dpi[1]

    if bits != (1,):
        ifd[BITSPERSAMPLE] = bits
        if len(bits) != 1:
            ifd[SAMPLESPERPIXEL] = len(bits)
    if extra is not None:
        ifd[EXTRASAMPLES] = extra
    if format != 1:
        ifd[SAMPLEFORMAT] = format

    if PHOTOMETRIC_INTERPRETATION not in ifd:
        ifd[PHOTOMETRIC_INTERPRETATION] = photo
    elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
        if im.mode == "1":
            inverted_im = im.copy()
            px = inverted_im.load()
            for y in range(inverted_im.height):
                for x in range(inverted_im.width):
                    px[x, y] = 0 if px[x, y] == 255 else 255
            im = inverted_im
        else:
            im = ImageOps.invert(im)

    if im.mode in ["P", "PA"]:
        lut = im.im.getpalette("RGB", "RGB;L")
        colormap = []
        colors = len(lut) // 3
        for i in range(3):
            colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]
            colormap += [0] * (256 - colors)
        ifd[COLORMAP] = colormap
    # data orientation
    stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
    # aim for given strip size (64 KB by default) when using libtiff writer
    if libtiff:
        im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
        rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
        # JPEG encoder expects multiple of 8 rows
        if compression == "jpeg":
            rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
    else:
        rows_per_strip = im.size[1]
    if rows_per_strip == 0:
        rows_per_strip = 1
    strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
    strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
    ifd[ROWSPERSTRIP] = rows_per_strip
    if strip_byte_counts >= 2**16:
        ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
    ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
        stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
    )
    ifd[STRIPOFFSETS] = tuple(
        range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
    )  # this is adjusted by IFD writer
    # no compression by default:
    ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)

    if im.mode == "YCbCr":
        for tag, value in {
            YCBCRSUBSAMPLING: (1, 1),
            REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
        }.items():
            ifd.setdefault(tag, value)

    blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
    if libtiff:
        if "quality" in encoderinfo:
            quality = encoderinfo["quality"]
            if not isinstance(quality, int) or quality < 0 or quality > 100:
                raise ValueError("Invalid quality setting")
            if compression != "jpeg":
                raise ValueError(
                    "quality setting only supported for 'jpeg' compression"
                )
            ifd[JPEGQUALITY] = quality

        logger.debug("Saving using libtiff encoder")
        logger.debug("Items: %s" % sorted(ifd.items()))
        _fp = 0
        if hasattr(fp, "fileno"):
            try:
                fp.seek(0)
                _fp = os.dup(fp.fileno())
            except io.UnsupportedOperation:
                pass

        # optional types for non core tags
        types = {}
        # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
        # based on the data in the strip.
        # The other tags expect arrays with a certain length (fixed or depending on
        # BITSPERSAMPLE, etc), passing arrays with a different length will result in
        # segfaults. Block these tags until we add extra validation.
        # SUBIFD may also cause a segfault.
        blocklist += [
            REFERENCEBLACKWHITE,
            STRIPBYTECOUNTS,
            STRIPOFFSETS,
            TRANSFERFUNCTION,
            SUBIFD,
        ]

        # bits per sample is a single short in the tiff directory, not a list.
        atts = {BITSPERSAMPLE: bits[0]}
        # Merge the ones that we have with (optional) more bits from
        # the original file, e.g x,y resolution so that we can
        # save(load('')) == original file.
        legacy_ifd = {}
        if hasattr(im, "tag"):
            legacy_ifd = im.tag.to_v2()

        # SAMPLEFORMAT is determined by the image format and should not be copied
        # from legacy_ifd.
        supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
        if SAMPLEFORMAT in supplied_tags:
            del supplied_tags[SAMPLEFORMAT]

        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
            # Libtiff can only process certain core items without adding
            # them to the custom dictionary.
            # Custom items are supported for int, float, unicode, string and byte
            # values. Other types and tuples require a tagtype.
            if tag not in TiffTags.LIBTIFF_CORE:
                if not Image.core.libtiff_support_custom_tags:
                    continue

                if tag in ifd.tagtype:
                    types[tag] = ifd.tagtype[tag]
                elif not (isinstance(value, (int, float, str, bytes))):
                    continue
                else:
                    type = TiffTags.lookup(tag).type
                    if type:
                        types[tag] = type
            if tag not in atts and tag not in blocklist:
                if isinstance(value, str):
                    atts[tag] = value.encode("ascii", "replace") + b"\0"
                elif isinstance(value, IFDRational):
                    atts[tag] = float(value)
                else:
                    atts[tag] = value

        if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
            atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]

        logger.debug("Converted items: %s" % sorted(atts.items()))

        # libtiff always expects the bytes in native order.
        # we're storing image byte order. So, if the rawmode
        # contains I;16, we need to convert from native to image
        # byte order.
        if im.mode in ("I;16B", "I;16"):
            rawmode = "I;16N"

        # Pass tags as sorted list so that the tags are set in a fixed order.
        # This is required by libtiff for some tags. For example, the JPEGQUALITY
        # pseudo tag requires that the COMPRESS tag was already set.
        tags = list(atts.items())
        tags.sort()
        a = (rawmode, compression, _fp, filename, tags, types)
        e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
        e.setimage(im.im, (0, 0) + im.size)
        while True:
            # undone, change to self.decodermaxblock:
            l, s, d = e.encode(16 * 1024)
            if not _fp:
                fp.write(d)
            if s:
                break
        if s < 0:
            raise OSError(f"encoder error {s} when writing image file")

    else:
        for tag in blocklist:
            del ifd[tag]
        offset = ifd.save(fp)

        ImageFile._save(
            im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
        )

    # -- helper for multi-page save --
    if "_debug_multipage" in encoderinfo:
        # just to access o32 and o16 (using correct byte order)
        im._debug_multipage = ifd


class AppendingTiffWriter:
    fieldSizes = [
        0,  # None
        1,  # byte
        1,  # ascii
        2,  # short
        4,  # long
        8,  # rational
        1,  # sbyte
        1,  # undefined
        2,  # sshort
        4,  # slong
        8,  # srational
        4,  # float
        8,  # double
    ]

    #    StripOffsets = 273
    #    FreeOffsets = 288
    #    TileOffsets = 324
    #    JPEGQTables = 519
    #    JPEGDCTables = 520
    #    JPEGACTables = 521
    Tags = {273, 288, 324, 519, 520, 521}

    def __init__(self, fn, new=False):
        if hasattr(fn, "read"):
            self.f = fn
            self.close_fp = False
        else:
            self.name = fn
            self.close_fp = True
            try:
                self.f = open(fn, "w+b" if new else "r+b")
            except OSError:
                self.f = open(fn, "w+b")
        self.beginning = self.f.tell()
        self.setup()

    def setup(self):
        # Reset everything.
        self.f.seek(self.beginning, os.SEEK_SET)

        self.whereToWriteNewIFDOffset = None
        self.offsetOfNewPage = 0

        self.IIMM = iimm = self.f.read(4)
        if not iimm:
            # empty file - first page
            self.isFirst = True
            return

        self.isFirst = False
        if iimm == b"II\x2a\x00":
            self.setEndian("<")
        elif iimm == b"MM\x00\x2a":
            self.setEndian(">")
        else:
            raise RuntimeError("Invalid TIFF file header")

        self.skipIFDs()
        self.goToEnd()

    def finalize(self):
        if self.isFirst:
            return

        # fix offsets
        self.f.seek(self.offsetOfNewPage)

        iimm = self.f.read(4)
        if not iimm:
            # raise RuntimeError("nothing written into new page")
            # Make it easy to finish a frame without committing to a new one.
            return

        if iimm != self.IIMM:
            raise RuntimeError("IIMM of new page doesn't match IIMM of first page")

        ifd_offset = self.readLong()
        ifd_offset += self.offsetOfNewPage
        self.f.seek(self.whereToWriteNewIFDOffset)
        self.writeLong(ifd_offset)
        self.f.seek(ifd_offset)
        self.fixIFD()

    def newFrame(self):
        # Call this to finish a frame.
        self.finalize()
        self.setup()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if self.close_fp:
            self.close()
        return False

    def tell(self):
        return self.f.tell() - self.offsetOfNewPage

    def seek(self, offset, whence=io.SEEK_SET):
        if whence == os.SEEK_SET:
            offset += self.offsetOfNewPage

        self.f.seek(offset, whence)
        return self.tell()

    def goToEnd(self):
        self.f.seek(0, os.SEEK_END)
        pos = self.f.tell()

        # pad to 16 byte boundary
        pad_bytes = 16 - pos % 16
        if 0 < pad_bytes < 16:
            self.f.write(bytes(pad_bytes))
        self.offsetOfNewPage = self.f.tell()

    def setEndian(self, endian):
        self.endian = endian
        self.longFmt = self.endian + "L"
        self.shortFmt = self.endian + "H"
        self.tagFormat = self.endian + "HHL"

    def skipIFDs(self):
        while True:
            ifd_offset = self.readLong()
            if ifd_offset == 0:
                self.whereToWriteNewIFDOffset = self.f.tell() - 4
                break

            self.f.seek(ifd_offset)
            num_tags = self.readShort()
            self.f.seek(num_tags * 12, os.SEEK_CUR)

    def write(self, data):
        return self.f.write(data)

    def readShort(self):
        (value,) = struct.unpack(self.shortFmt, self.f.read(2))
        return value

    def readLong(self):
        (value,) = struct.unpack(self.longFmt, self.f.read(4))
        return value

    def rewriteLastShortToLong(self, value):
        self.f.seek(-2, os.SEEK_CUR)
        bytes_written = self.f.write(struct.pack(self.longFmt, value))
        if bytes_written is not None and bytes_written != 4:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")

    def rewriteLastShort(self, value):
        self.f.seek(-2, os.SEEK_CUR)
        bytes_written = self.f.write(struct.pack(self.shortFmt, value))
        if bytes_written is not None and bytes_written != 2:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")

    def rewriteLastLong(self, value):
        self.f.seek(-4, os.SEEK_CUR)
        bytes_written = self.f.write(struct.pack(self.longFmt, value))
        if bytes_written is not None and bytes_written != 4:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")

    def writeShort(self, value):
        bytes_written = self.f.write(struct.pack(self.shortFmt, value))
        if bytes_written is not None and bytes_written != 2:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")

    def writeLong(self, value):
        bytes_written = self.f.write(struct.pack(self.longFmt, value))
        if bytes_written is not None and bytes_written != 4:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")

    def close(self):
        self.finalize()
        self.f.close()

    def fixIFD(self):
        num_tags = self.readShort()

        for i in range(num_tags):
            tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))

            field_size = self.fieldSizes[field_type]
            total_size = field_size * count
            is_local = total_size <= 4
            if not is_local:
                offset = self.readLong()
                offset += self.offsetOfNewPage
                self.rewriteLastLong(offset)

            if tag in self.Tags:
                cur_pos = self.f.tell()

                if is_local:
                    self.fixOffsets(
                        count, isShort=(field_size == 2), isLong=(field_size == 4)
                    )
                    self.f.seek(cur_pos + 4)
                else:
                    self.f.seek(offset)
                    self.fixOffsets(
                        count, isShort=(field_size == 2), isLong=(field_size == 4)
                    )
                    self.f.seek(cur_pos)

                offset = cur_pos = None

            elif is_local:
                # skip the locally stored value that is not an offset
                self.f.seek(4, os.SEEK_CUR)

    def fixOffsets(self, count, isShort=False, isLong=False):
        if not isShort and not isLong:
            raise RuntimeError("offset is neither short nor long")

        for i in range(count):
            offset = self.readShort() if isShort else self.readLong()
            offset += self.offsetOfNewPage
            if isShort and offset >= 65536:
                # offset is now too large - we must convert shorts to longs
                if count != 1:
                    raise RuntimeError("not implemented")  # XXX TODO

                # simple case - the offset is just one and therefore it is
                # local (not referenced with another offset)
                self.rewriteLastShortToLong(offset)
                self.f.seek(-10, os.SEEK_CUR)
                self.writeShort(TiffTags.LONG)  # rewrite the type to LONG
                self.f.seek(8, os.SEEK_CUR)
            elif isShort:
                self.rewriteLastShort(offset)
            else:
                self.rewriteLastLong(offset)


def _save_all(im, fp, filename):
    encoderinfo = im.encoderinfo.copy()
    encoderconfig = im.encoderconfig
    append_images = list(encoderinfo.get("append_images", []))
    if not hasattr(im, "n_frames") and not append_images:
        return _save(im, fp, filename)

    cur_idx = im.tell()
    try:
        with AppendingTiffWriter(fp) as tf:
            for ims in [im] + append_images:
                ims.encoderinfo = encoderinfo
                ims.encoderconfig = encoderconfig
                if not hasattr(ims, "n_frames"):
                    nfr = 1
                else:
                    nfr = ims.n_frames

                for idx in range(nfr):
                    ims.seek(idx)
                    ims.load()
                    _save(ims, tf, filename)
                    tf.newFrame()
    finally:
        im.seek(cur_idx)


#
# --------------------------------------------------------------------
# Register

Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
Image.register_save(TiffImageFile.format, _save)
Image.register_save_all(TiffImageFile.format, _save_all)

Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])

Image.register_mime(TiffImageFile.format, "image/tiff")

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
[ISSUE]
**Title:** Writing integer values as ASCII tags raises AttributeError

**Description**
When assigning an integer to an ASCII tag in TIFF metadata and attempting to save the image, an `AttributeError` is triggered because integers do not have an `encode` method. This issue prevents users from using integer values for ASCII tags, which can be necessary for certain metadata specifications.

**Example Code**
```python
from PIL import Image, TiffImagePlugin, TiffTags

# Create a new image
im = Image.new("RGB", (100, 100))
info = TiffImagePlugin.ImageFileDirectory_v2()

# Assign an integer to an ASCII tag
tag = TiffTags.TAGS_V2[271]  # Example tag for ImageDescription
info[271] = 123  # Integer value

# Attempt to save the image with the TIFF metadata
out = "temp.tiff"
im.save(out, tiffinfo=info)
```

**Expected Behavior**
The integer value should be automatically converted to a string and saved as an ASCII tag in the TIFF metadata without any errors.

**Actual Behavior**
An `AttributeError` is raised with the message: `'int' object has no attribute 'encode'`.

[/ISSUE]
{
    "\u001b[1mtest_rt_metadata\u001b[0m": "PASSED",
    "\u001b[1mtest_read_metadata\u001b[0m": "PASSED",
    "\u001b[1mtest_write_metadata\u001b[0m": "PASSED",
    "\u001b[1mtest_change_stripbytecounts_tag_type\u001b[0m": "PASSED",
    "\u001b[1mtest_no_duplicate_50741_tag\u001b[0m": "PASSED",
    "\u001b[1mtest_iptc\u001b[0m": "PASSED",
    "\u001b[1mtest_writing_other_types_to_ascii\u001b[0m": "PASSED",
    "\u001b[1mtest_writing_int_to_bytes\u001b[0m": "PASSED",
    "\u001b[1mtest_undefined_zero\u001b[0m": "PASSED",
    "\u001b[1mtest_empty_metadata\u001b[0m": "PASSED",
    "\u001b[1mtest_iccprofile\u001b[0m": "PASSED",
    "\u001b[1mtest_iccprofile_binary\u001b[0m": "PASSED",
    "\u001b[1mtest_iccprofile_save_png\u001b[0m": "PASSED",
    "\u001b[1mtest_iccprofile_binary_save_png\u001b[0m": "PASSED",
    "\u001b[1mtest_exif_div_zero\u001b[0m": "PASSED",
    "\u001b[1mtest_ifd_unsigned_rational\u001b[0m": "PASSED",
    "\u001b[1mtest_ifd_signed_rational\u001b[0m": "PASSED",
    "\u001b[1mtest_ifd_signed_long\u001b[0m": "PASSED",
    "\u001b[1mtest_empty_values\u001b[0m": "PASSED",
    "\u001b[1mtest_photoshop_info\u001b[0m": "PASSED",
    "\u001b[1mtest_too_many_entries\u001b[0m": "PASSED",
    "\u001b[1mtest_tag_group_data\u001b[0m": "PASSED",
    "\u001b[1mtest_empty_subifd\u001b[0m": "PASSED"
}
diff --git a/src/PIL/TiffImagePlugin.py b/src/PIL/TiffImagePlugin.py
index ab9ac5ea2..791e692c1 100644
--- a/src/PIL/TiffImagePlugin.py
+++ b/src/PIL/TiffImagePlugin.py
@@ -732,6 +732,8 @@ class ImageFileDirectory_v2(MutableMapping):
     @_register_writer(2)
     def write_string(self, value):
         # remerge of https://github.com/python-pillow/Pillow/pull/1416
+        if isinstance(value, int):
+            value = str(value)
         if not isinstance(value, bytes):
             value = value.encode("ascii", "replace")
         return value + b"\0"
 

{"src/PIL/TiffImagePlugin.py": "#\n# The Python Imaging Library.\n# $Id$\n#\n# TIFF file handling\n#\n# TIFF is a flexible, if somewhat aged, image file format originally\n# defined by Aldus.  Although TIFF supports a wide variety of pixel\n# layouts and compression methods, the name doesn't really stand for\n# \"thousands of incompatible file formats,\" it just feels that way.\n#\n# To read TIFF data from a stream, the stream must be seekable.  For\n# progressive decoding, make sure to use TIFF files where the tag\n# directory is placed first in the file.\n#\n# History:\n# 1995-09-01 fl   Created\n# 1996-05-04 fl   Handle JPEGTABLES tag\n# 1996-05-18 fl   Fixed COLORMAP support\n# 1997-01-05 fl   Fixed PREDICTOR support\n# 1997-08-27 fl   Added support for rational tags (from Perry Stoll)\n# 1998-01-10 fl   Fixed seek/tell (from Jan Blom)\n# 1998-07-15 fl   Use private names for internal variables\n# 1999-06-13 fl   Rewritten for PIL 1.0 (1.0)\n# 2000-10-11 fl   Additional fixes for Python 2.0 (1.1)\n# 2001-04-17 fl   Fixed rewind support (seek to frame 0) (1.2)\n# 2001-05-12 fl   Added write support for more tags (from Greg Couch) (1.3)\n# 2001-12-18 fl   Added workaround for broken Matrox library\n# 2002-01-18 fl   Don't mess up if photometric tag is missing (D. Alan Stewart)\n# 2003-05-19 fl   Check FILLORDER tag\n# 2003-09-26 fl   Added RGBa support\n# 2004-02-24 fl   Added DPI support; fixed rational write support\n# 2005-02-07 fl   Added workaround for broken Corel Draw 10 files\n# 2006-01-09 fl   Added support for float/double tags (from Russell Nelson)\n#\n# Copyright (c) 1997-2006 by Secret Labs AB.  All rights reserved.\n# Copyright (c) 1995-1997 by Fredrik Lundh\n#\n# See the README file for information on usage and redistribution.\n#\nimport io\nimport itertools\nimport logging\nimport math\nimport os\nimport struct\nimport warnings\nfrom collections.abc import MutableMapping\nfrom fractions import Fraction\nfrom numbers import Number, Rational\n\nfrom . import Image, ImageFile, ImageOps, ImagePalette, TiffTags\nfrom ._binary import i16be as i16\nfrom ._binary import i32be as i32\nfrom ._binary import o8\nfrom .TiffTags import TYPES\n\nlogger = logging.getLogger(__name__)\n\n# Set these to true to force use of libtiff for reading or writing.\nREAD_LIBTIFF = False\nWRITE_LIBTIFF = False\nIFD_LEGACY_API = True\nSTRIP_SIZE = 65536\n\nII = b\"II\"  # little-endian (Intel style)\nMM = b\"MM\"  # big-endian (Motorola style)\n\n#\n# --------------------------------------------------------------------\n# Read TIFF files\n\n# a few tag names, just to make the code below a bit more readable\nIMAGEWIDTH = 256\nIMAGELENGTH = 257\nBITSPERSAMPLE = 258\nCOMPRESSION = 259\nPHOTOMETRIC_INTERPRETATION = 262\nFILLORDER = 266\nIMAGEDESCRIPTION = 270\nSTRIPOFFSETS = 273\nSAMPLESPERPIXEL = 277\nROWSPERSTRIP = 278\nSTRIPBYTECOUNTS = 279\nX_RESOLUTION = 282\nY_RESOLUTION = 283\nPLANAR_CONFIGURATION = 284\nRESOLUTION_UNIT = 296\nTRANSFERFUNCTION = 301\nSOFTWARE = 305\nDATE_TIME = 306\nARTIST = 315\nPREDICTOR = 317\nCOLORMAP = 320\nTILEWIDTH = 322\nTILELENGTH = 323\nTILEOFFSETS = 324\nTILEBYTECOUNTS = 325\nSUBIFD = 330\nEXTRASAMPLES = 338\nSAMPLEFORMAT = 339\nJPEGTABLES = 347\nYCBCRSUBSAMPLING = 530\nREFERENCEBLACKWHITE = 532\nCOPYRIGHT = 33432\nIPTC_NAA_CHUNK = 33723  # newsphoto properties\nPHOTOSHOP_CHUNK = 34377  # photoshop properties\nICCPROFILE = 34675\nEXIFIFD = 34665\nXMP = 700\nJPEGQUALITY = 65537  # pseudo-tag by libtiff\n\n# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java\nIMAGEJ_META_DATA_BYTE_COUNTS = 50838\nIMAGEJ_META_DATA = 50839\n\nCOMPRESSION_INFO = {\n    # Compression => pil compression name\n    1: \"raw\",\n    2: \"tiff_ccitt\",\n    3: \"group3\",\n    4: \"group4\",\n    5: \"tiff_lzw\",\n    6: \"tiff_jpeg\",  # obsolete\n    7: \"jpeg\",\n    8: \"tiff_adobe_deflate\",\n    32771: \"tiff_raw_16\",  # 16-bit padding\n    32773: \"packbits\",\n    32809: \"tiff_thunderscan\",\n    32946: \"tiff_deflate\",\n    34676: \"tiff_sgilog\",\n    34677: \"tiff_sgilog24\",\n    34925: \"lzma\",\n    50000: \"zstd\",\n    50001: \"webp\",\n}\n\nCOMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}\n\nOPEN_INFO = {\n    # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,\n    #  ExtraSamples) => mode, rawmode\n    (II, 0, (1,), 1, (1,), ()): (\"1\", \"1;I\"),\n    (MM, 0, (1,), 1, (1,), ()): (\"1\", \"1;I\"),\n    (II, 0, (1,), 2, (1,), ()): (\"1\", \"1;IR\"),\n    (MM, 0, (1,), 2, (1,), ()): (\"1\", \"1;IR\"),\n    (II, 1, (1,), 1, (1,), ()): (\"1\", \"1\"),\n    (MM, 1, (1,), 1, (1,), ()): (\"1\", \"1\"),\n    (II, 1, (1,), 2, (1,), ()): (\"1\", \"1;R\"),\n    (MM, 1, (1,), 2, (1,), ()): (\"1\", \"1;R\"),\n    (II, 0, (1,), 1, (2,), ()): (\"L\", \"L;2I\"),\n    (MM, 0, (1,), 1, (2,), ()): (\"L\", \"L;2I\"),\n    (II, 0, (1,), 2, (2,), ()): (\"L\", \"L;2IR\"),\n    (MM, 0, (1,), 2, (2,), ()): (\"L\", \"L;2IR\"),\n    (II, 1, (1,), 1, (2,), ()): (\"L\", \"L;2\"),\n    (MM, 1, (1,), 1, (2,), ()): (\"L\", \"L;2\"),\n    (II, 1, (1,), 2, (2,), ()): (\"L\", \"L;2R\"),\n    (MM, 1, (1,), 2, (2,), ()): (\"L\", \"L;2R\"),\n    (II, 0, (1,), 1, (4,), ()): (\"L\", \"L;4I\"),\n    (MM, 0, (1,), 1, (4,), ()): (\"L\", \"L;4I\"),\n    (II, 0, (1,), 2, (4,), ()): (\"L\", \"L;4IR\"),\n    (MM, 0, (1,), 2, (4,), ()): (\"L\", \"L;4IR\"),\n    (II, 1, (1,), 1, (4,), ()): (\"L\", \"L;4\"),\n    (MM, 1, (1,), 1, (4,), ()): (\"L\", \"L;4\"),\n    (II, 1, (1,), 2, (4,), ()): (\"L\", \"L;4R\"),\n    (MM, 1, (1,), 2, (4,), ()): (\"L\", \"L;4R\"),\n    (II, 0, (1,), 1, (8,), ()): (\"L\", \"L;I\"),\n    (MM, 0, (1,), 1, (8,), ()): (\"L\", \"L;I\"),\n    (II, 0, (1,), 2, (8,), ()): (\"L\", \"L;IR\"),\n    (MM, 0, (1,), 2, (8,), ()): (\"L\", \"L;IR\"),\n    (II, 1, (1,), 1, (8,), ()): (\"L\", \"L\"),\n    (MM, 1, (1,), 1, (8,), ()): (\"L\", \"L\"),\n    (II, 1, (1,), 2, (8,), ()): (\"L\", \"L;R\"),\n    (MM, 1, (1,), 2, (8,), ()): (\"L\", \"L;R\"),\n    (II, 1, (1,), 1, (12,), ()): (\"I;16\", \"I;12\"),\n    (II, 0, (1,), 1, (16,), ()): (\"I;16\", \"I;16\"),\n    (II, 1, (1,), 1, (16,), ()): (\"I;16\", \"I;16\"),\n    (MM, 1, (1,), 1, (16,), ()): (\"I;16B\", \"I;16B\"),\n    (II, 1, (1,), 2, (16,), ()): (\"I;16\", \"I;16R\"),\n    (II, 1, (2,), 1, (16,), ()): (\"I\", \"I;16S\"),\n    (MM, 1, (2,), 1, (16,), ()): (\"I\", \"I;16BS\"),\n    (II, 0, (3,), 1, (32,), ()): (\"F\", \"F;32F\"),\n    (MM, 0, (3,), 1, (32,), ()): (\"F\", \"F;32BF\"),\n    (II, 1, (1,), 1, (32,), ()): (\"I\", \"I;32N\"),\n    (II, 1, (2,), 1, (32,), ()): (\"I\", \"I;32S\"),\n    (MM, 1, (2,), 1, (32,), ()): (\"I\", \"I;32BS\"),\n    (II, 1, (3,), 1, (32,), ()): (\"F\", \"F;32F\"),\n    (MM, 1, (3,), 1, (32,), ()): (\"F\", \"F;32BF\"),\n    (II, 1, (1,), 1, (8, 8), (2,)): (\"LA\", \"LA\"),\n    (MM, 1, (1,), 1, (8, 8), (2,)): (\"LA\", \"LA\"),\n    (II, 2, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGB\"),\n    (MM, 2, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGB\"),\n    (II, 2, (1,), 2, (8, 8, 8), ()): (\"RGB\", \"RGB;R\"),\n    (MM, 2, (1,), 2, (8, 8, 8), ()): (\"RGB\", \"RGB;R\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), ()): (\"RGBA\", \"RGBA\"),  # missing ExtraSamples\n    (MM, 2, (1,), 1, (8, 8, 8, 8), ()): (\"RGBA\", \"RGBA\"),  # missing ExtraSamples\n    (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): (\"RGBX\", \"RGBX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): (\"RGBX\", \"RGBX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): (\"RGBX\", \"RGBXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): (\"RGBX\", \"RGBXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): (\"RGBX\", \"RGBXXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): (\"RGBX\", \"RGBXXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): (\"RGBA\", \"RGBa\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): (\"RGBA\", \"RGBa\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): (\"RGBA\", \"RGBaX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): (\"RGBA\", \"RGBaX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): (\"RGBA\", \"RGBaXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): (\"RGBA\", \"RGBaXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): (\"RGBA\", \"RGBA\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): (\"RGBA\", \"RGBA\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): (\"RGBA\", \"RGBAX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): (\"RGBA\", \"RGBAX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): (\"RGBA\", \"RGBAXX\"),\n    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): (\"RGBA\", \"RGBAXX\"),\n    (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): (\"RGBA\", \"RGBA\"),  # Corel Draw 10\n    (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): (\"RGBA\", \"RGBA\"),  # Corel Draw 10\n    (II, 2, (1,), 1, (16, 16, 16), ()): (\"RGB\", \"RGB;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16), ()): (\"RGB\", \"RGB;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), ()): (\"RGBA\", \"RGBA;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), ()): (\"RGBA\", \"RGBA;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): (\"RGBX\", \"RGBX;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): (\"RGBX\", \"RGBX;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): (\"RGBA\", \"RGBa;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): (\"RGBA\", \"RGBa;16B\"),\n    (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): (\"RGBA\", \"RGBA;16L\"),\n    (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): (\"RGBA\", \"RGBA;16B\"),\n    (II, 3, (1,), 1, (1,), ()): (\"P\", \"P;1\"),\n    (MM, 3, (1,), 1, (1,), ()): (\"P\", \"P;1\"),\n    (II, 3, (1,), 2, (1,), ()): (\"P\", \"P;1R\"),\n    (MM, 3, (1,), 2, (1,), ()): (\"P\", \"P;1R\"),\n    (II, 3, (1,), 1, (2,), ()): (\"P\", \"P;2\"),\n    (MM, 3, (1,), 1, (2,), ()): (\"P\", \"P;2\"),\n    (II, 3, (1,), 2, (2,), ()): (\"P\", \"P;2R\"),\n    (MM, 3, (1,), 2, (2,), ()): (\"P\", \"P;2R\"),\n    (II, 3, (1,), 1, (4,), ()): (\"P\", \"P;4\"),\n    (MM, 3, (1,), 1, (4,), ()): (\"P\", \"P;4\"),\n    (II, 3, (1,), 2, (4,), ()): (\"P\", \"P;4R\"),\n    (MM, 3, (1,), 2, (4,), ()): (\"P\", \"P;4R\"),\n    (II, 3, (1,), 1, (8,), ()): (\"P\", \"P\"),\n    (MM, 3, (1,), 1, (8,), ()): (\"P\", \"P\"),\n    (II, 3, (1,), 1, (8, 8), (2,)): (\"PA\", \"PA\"),\n    (MM, 3, (1,), 1, (8, 8), (2,)): (\"PA\", \"PA\"),\n    (II, 3, (1,), 2, (8,), ()): (\"P\", \"P;R\"),\n    (MM, 3, (1,), 2, (8,), ()): (\"P\", \"P;R\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8), ()): (\"CMYK\", \"CMYK\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8), ()): (\"CMYK\", \"CMYK\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): (\"CMYK\", \"CMYKX\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): (\"CMYK\", \"CMYKX\"),\n    (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): (\"CMYK\", \"CMYKXX\"),\n    (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): (\"CMYK\", \"CMYKXX\"),\n    (II, 5, (1,), 1, (16, 16, 16, 16), ()): (\"CMYK\", \"CMYK;16L\"),\n    # JPEG compressed images handled by LibTiff and auto-converted to RGBX\n    # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel\n    (II, 6, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGBX\"),\n    (MM, 6, (1,), 1, (8, 8, 8), ()): (\"RGB\", \"RGBX\"),\n    (II, 8, (1,), 1, (8, 8, 8), ()): (\"LAB\", \"LAB\"),\n    (MM, 8, (1,), 1, (8, 8, 8), ()): (\"LAB\", \"LAB\"),\n}\n\nMAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())\n\nPREFIXES = [\n    b\"MM\\x00\\x2A\",  # Valid TIFF header with big-endian byte order\n    b\"II\\x2A\\x00\",  # Valid TIFF header with little-endian byte order\n    b\"MM\\x2A\\x00\",  # Invalid TIFF header, assume big-endian\n    b\"II\\x00\\x2A\",  # Invalid TIFF header, assume little-endian\n    b\"MM\\x00\\x2B\",  # BigTIFF with big-endian byte order\n    b\"II\\x2B\\x00\",  # BigTIFF with little-endian byte order\n]\n\n\ndef _accept(prefix):\n    return prefix[:4] in PREFIXES\n\n\ndef _limit_rational(val, max_val):\n    inv = abs(val) > 1\n    n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)\n    return n_d[::-1] if inv else n_d\n\n\ndef _limit_signed_rational(val, max_val, min_val):\n    frac = Fraction(val)\n    n_d = frac.numerator, frac.denominator\n\n    if min(n_d) < min_val:\n        n_d = _limit_rational(val, abs(min_val))\n\n    if max(n_d) > max_val:\n        val = Fraction(*n_d)\n        n_d = _limit_rational(val, max_val)\n\n    return n_d\n\n\n##\n# Wrapper for TIFF IFDs.\n\n_load_dispatch = {}\n_write_dispatch = {}\n\n\nclass IFDRational(Rational):\n    \"\"\"Implements a rational class where 0/0 is a legal value to match\n    the in the wild use of exif rationals.\n\n    e.g., DigitalZoomRatio - 0.00/0.00  indicates that no digital zoom was used\n    \"\"\"\n\n    \"\"\" If the denominator is 0, store this as a float('nan'), otherwise store\n    as a fractions.Fraction(). Delegate as appropriate\n\n    \"\"\"\n\n    __slots__ = (\"_numerator\", \"_denominator\", \"_val\")\n\n    def __init__(self, value, denominator=1):\n        \"\"\"\n        :param value: either an integer numerator, a\n        float/rational/other number, or an IFDRational\n        :param denominator: Optional integer denominator\n        \"\"\"\n        if isinstance(value, IFDRational):\n            self._numerator = value.numerator\n            self._denominator = value.denominator\n            self._val = value._val\n            return\n\n        if isinstance(value, Fraction):\n            self._numerator = value.numerator\n            self._denominator = value.denominator\n        else:\n            self._numerator = value\n            self._denominator = denominator\n\n        if denominator == 0:\n            self._val = float(\"nan\")\n        elif denominator == 1:\n            self._val = Fraction(value)\n        else:\n            self._val = Fraction(value, denominator)\n\n    @property\n    def numerator(self):\n        return self._numerator\n\n    @property\n    def denominator(self):\n        return self._denominator\n\n    def limit_rational(self, max_denominator):\n        \"\"\"\n\n        :param max_denominator: Integer, the maximum denominator value\n        :returns: Tuple of (numerator, denominator)\n        \"\"\"\n\n        if self.denominator == 0:\n            return self.numerator, self.denominator\n\n        f = self._val.limit_denominator(max_denominator)\n        return f.numerator, f.denominator\n\n    def __repr__(self):\n        return str(float(self._val))\n\n    def __hash__(self):\n        return self._val.__hash__()\n\n    def __eq__(self, other):\n        val = self._val\n        if isinstance(other, IFDRational):\n            other = other._val\n        if isinstance(other, float):\n            val = float(val)\n        return val == other\n\n    def __getstate__(self):\n        return [self._val, self._numerator, self._denominator]\n\n    def __setstate__(self, state):\n        IFDRational.__init__(self, 0)\n        _val, _numerator, _denominator = state\n        self._val = _val\n        self._numerator = _numerator\n        self._denominator = _denominator\n\n    def _delegate(op):\n        def delegate(self, *args):\n            return getattr(self._val, op)(*args)\n\n        return delegate\n\n    \"\"\" a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',\n             'truediv', 'rtruediv', 'floordiv', 'rfloordiv',\n             'mod','rmod', 'pow','rpow', 'pos', 'neg',\n             'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',\n             'ceil', 'floor', 'round']\n        print(\"\\n\".join(\"__%s__ = _delegate('__%s__')\" % (s,s) for s in a))\n        \"\"\"\n\n    __add__ = _delegate(\"__add__\")\n    __radd__ = _delegate(\"__radd__\")\n    __sub__ = _delegate(\"__sub__\")\n    __rsub__ = _delegate(\"__rsub__\")\n    __mul__ = _delegate(\"__mul__\")\n    __rmul__ = _delegate(\"__rmul__\")\n    __truediv__ = _delegate(\"__truediv__\")\n    __rtruediv__ = _delegate(\"__rtruediv__\")\n    __floordiv__ = _delegate(\"__floordiv__\")\n    __rfloordiv__ = _delegate(\"__rfloordiv__\")\n    __mod__ = _delegate(\"__mod__\")\n    __rmod__ = _delegate(\"__rmod__\")\n    __pow__ = _delegate(\"__pow__\")\n    __rpow__ = _delegate(\"__rpow__\")\n    __pos__ = _delegate(\"__pos__\")\n    __neg__ = _delegate(\"__neg__\")\n    __abs__ = _delegate(\"__abs__\")\n    __trunc__ = _delegate(\"__trunc__\")\n    __lt__ = _delegate(\"__lt__\")\n    __gt__ = _delegate(\"__gt__\")\n    __le__ = _delegate(\"__le__\")\n    __ge__ = _delegate(\"__ge__\")\n    __bool__ = _delegate(\"__bool__\")\n    __ceil__ = _delegate(\"__ceil__\")\n    __floor__ = _delegate(\"__floor__\")\n    __round__ = _delegate(\"__round__\")\n\n\nclass ImageFileDirectory_v2(MutableMapping):\n    \"\"\"This class represents a TIFF tag directory.  To speed things up, we\n    don't decode tags unless they're asked for.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v2()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        'Some Data'\n\n    Individual values are returned as the strings or numbers, sequences are\n    returned as tuples of the values.\n\n    The tiff metadata type of each item is stored in a dictionary of\n    tag types in\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types\n    are read from a tiff file, guessed from the type added, or added\n    manually.\n\n    Data Structures:\n\n        * ``self.tagtype = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: integer corresponding to the data type from\n            :py:data:`.TiffTags.TYPES`\n\n          .. versionadded:: 3.0.0\n\n    'Internal' data structures:\n\n        * ``self._tags_v2 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data, as tuple for multiple values\n\n        * ``self._tagdata = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: undecoded byte string from file\n\n        * ``self._tags_v1 = {}``\n\n          * Key: numerical TIFF tag number\n          * Value: decoded data in the v1 format\n\n    Tags will be found in the private attributes ``self._tagdata``, and in\n    ``self._tags_v2`` once decoded.\n\n    ``self.legacy_api`` is a value for internal use, and shouldn't be changed\n    from outside code. In cooperation with\n    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``\n    is true, then decoded tags will be populated into both ``_tags_v1`` and\n    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF\n    save routine. Tags should be read from ``_tags_v1`` if\n    ``legacy_api == true``.\n\n    \"\"\"\n\n    def __init__(self, ifh=b\"II\\052\\0\\0\\0\\0\\0\", prefix=None, group=None):\n        \"\"\"Initialize an ImageFileDirectory.\n\n        To construct an ImageFileDirectory from a real file, pass the 8-byte\n        magic header to the constructor.  To only set the endianness, pass it\n        as the 'prefix' keyword argument.\n\n        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets\n              endianness.\n        :param prefix: Override the endianness of the file.\n        \"\"\"\n        if not _accept(ifh):\n            raise SyntaxError(f\"not a TIFF file (header {repr(ifh)} not valid)\")\n        self._prefix = prefix if prefix is not None else ifh[:2]\n        if self._prefix == MM:\n            self._endian = \">\"\n        elif self._prefix == II:\n            self._endian = \"<\"\n        else:\n            raise SyntaxError(\"not a TIFF IFD\")\n        self._bigtiff = ifh[2] == 43\n        self.group = group\n        self.tagtype = {}\n        \"\"\" Dictionary of tag types \"\"\"\n        self.reset()\n        (self.next,) = (\n            self._unpack(\"Q\", ifh[8:]) if self._bigtiff else self._unpack(\"L\", ifh[4:])\n        )\n        self._legacy_api = False\n\n    prefix = property(lambda self: self._prefix)\n    offset = property(lambda self: self._offset)\n    legacy_api = property(lambda self: self._legacy_api)\n\n    @legacy_api.setter\n    def legacy_api(self, value):\n        raise Exception(\"Not allowing setting of legacy api\")\n\n    def reset(self):\n        self._tags_v1 = {}  # will remain empty if legacy_api is false\n        self._tags_v2 = {}  # main tag storage\n        self._tagdata = {}\n        self.tagtype = {}  # added 2008-06-05 by Florian Hoech\n        self._next = None\n        self._offset = None\n\n    def __str__(self):\n        return str(dict(self))\n\n    def named(self):\n        \"\"\"\n        :returns: dict of name|key: value\n\n        Returns the complete tag dictionary, with named tags where possible.\n        \"\"\"\n        return {\n            TiffTags.lookup(code, self.group).name: value\n            for code, value in self.items()\n        }\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v2))\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v2:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            self[tag] = handler(self, data, self.legacy_api)  # check type\n        val = self._tags_v2[tag]\n        if self.legacy_api and not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n    def __contains__(self, tag):\n        return tag in self._tags_v2 or tag in self._tagdata\n\n    def __setitem__(self, tag, value):\n        self._setitem(tag, value, self.legacy_api)\n\n    def _setitem(self, tag, value, legacy_api):\n        basetypes = (Number, bytes, str)\n\n        info = TiffTags.lookup(tag, self.group)\n        values = [value] if isinstance(value, basetypes) else value\n\n        if tag not in self.tagtype:\n            if info.type:\n                self.tagtype[tag] = info.type\n            else:\n                self.tagtype[tag] = TiffTags.UNDEFINED\n                if all(isinstance(v, IFDRational) for v in values):\n                    self.tagtype[tag] = (\n                        TiffTags.RATIONAL\n                        if all(v >= 0 for v in values)\n                        else TiffTags.SIGNED_RATIONAL\n                    )\n                elif all(isinstance(v, int) for v in values):\n                    if all(0 <= v < 2**16 for v in values):\n                        self.tagtype[tag] = TiffTags.SHORT\n                    elif all(-(2**15) < v < 2**15 for v in values):\n                        self.tagtype[tag] = TiffTags.SIGNED_SHORT\n                    else:\n                        self.tagtype[tag] = (\n                            TiffTags.LONG\n                            if all(v >= 0 for v in values)\n                            else TiffTags.SIGNED_LONG\n                        )\n                elif all(isinstance(v, float) for v in values):\n                    self.tagtype[tag] = TiffTags.DOUBLE\n                elif all(isinstance(v, str) for v in values):\n                    self.tagtype[tag] = TiffTags.ASCII\n                elif all(isinstance(v, bytes) for v in values):\n                    self.tagtype[tag] = TiffTags.BYTE\n\n        if self.tagtype[tag] == TiffTags.UNDEFINED:\n            values = [\n                v.encode(\"ascii\", \"replace\") if isinstance(v, str) else v\n                for v in values\n            ]\n        elif self.tagtype[tag] == TiffTags.RATIONAL:\n            values = [float(v) if isinstance(v, int) else v for v in values]\n\n        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)\n        if not is_ifd:\n            values = tuple(info.cvt_enum(value) for value in values)\n\n        dest = self._tags_v1 if legacy_api else self._tags_v2\n\n        # Three branches:\n        # Spec'd length == 1, Actual length 1, store as element\n        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.\n        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.\n        # Don't mess with the legacy api, since it's frozen.\n        if not is_ifd and (\n            (info.length == 1)\n            or self.tagtype[tag] == TiffTags.BYTE\n            or (info.length is None and len(values) == 1 and not legacy_api)\n        ):\n            # Don't mess with the legacy api, since it's frozen.\n            if legacy_api and self.tagtype[tag] in [\n                TiffTags.RATIONAL,\n                TiffTags.SIGNED_RATIONAL,\n            ]:  # rationals\n                values = (values,)\n            try:\n                (dest[tag],) = values\n            except ValueError:\n                # We've got a builtin tag with 1 expected entry\n                warnings.warn(\n                    f\"Metadata Warning, tag {tag} had too many entries: \"\n                    f\"{len(values)}, expected 1\"\n                )\n                dest[tag] = values[0]\n\n        else:\n            # Spec'd length > 1 or undefined\n            # Unspec'd, and length > 1\n            dest[tag] = values\n\n    def __delitem__(self, tag):\n        self._tags_v2.pop(tag, None)\n        self._tags_v1.pop(tag, None)\n        self._tagdata.pop(tag, None)\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v2))\n\n    def _unpack(self, fmt, data):\n        return struct.unpack(self._endian + fmt, data)\n\n    def _pack(self, fmt, *values):\n        return struct.pack(self._endian + fmt, *values)\n\n    def _register_loader(idx, size):\n        def decorator(func):\n            from .TiffTags import TYPES\n\n            if func.__name__.startswith(\"load_\"):\n                TYPES[idx] = func.__name__[5:].replace(\"_\", \" \")\n            _load_dispatch[idx] = size, func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_writer(idx):\n        def decorator(func):\n            _write_dispatch[idx] = func  # noqa: F821\n            return func\n\n        return decorator\n\n    def _register_basic(idx_fmt_name):\n        from .TiffTags import TYPES\n\n        idx, fmt, name = idx_fmt_name\n        TYPES[idx] = name\n        size = struct.calcsize(\"=\" + fmt)\n        _load_dispatch[idx] = (  # noqa: F821\n            size,\n            lambda self, data, legacy_api=True: (\n                self._unpack(f\"{len(data) // size}{fmt}\", data)\n            ),\n        )\n        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821\n            b\"\".join(self._pack(fmt, value) for value in values)\n        )\n\n    list(\n        map(\n            _register_basic,\n            [\n                (TiffTags.SHORT, \"H\", \"short\"),\n                (TiffTags.LONG, \"L\", \"long\"),\n                (TiffTags.SIGNED_BYTE, \"b\", \"signed byte\"),\n                (TiffTags.SIGNED_SHORT, \"h\", \"signed short\"),\n                (TiffTags.SIGNED_LONG, \"l\", \"signed long\"),\n                (TiffTags.FLOAT, \"f\", \"float\"),\n                (TiffTags.DOUBLE, \"d\", \"double\"),\n                (TiffTags.IFD, \"L\", \"long\"),\n                (TiffTags.LONG8, \"Q\", \"long8\"),\n            ],\n        )\n    )\n\n    @_register_loader(1, 1)  # Basic type, except for the legacy API.\n    def load_byte(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(1)  # Basic type, except for the legacy API.\n    def write_byte(self, data):\n        if isinstance(data, int):\n            data = bytes((data,))\n        return data\n\n    @_register_loader(2, 1)\n    def load_string(self, data, legacy_api=True):\n        if data.endswith(b\"\\0\"):\n            data = data[:-1]\n        return data.decode(\"latin-1\", \"replace\")\n\n    @_register_writer(2)\n    def write_string(self, value):\n        # remerge of https://github.com/python-pillow/Pillow/pull/1416\n        if not isinstance(value, bytes):\n            value = value.encode(\"ascii\", \"replace\")\n        return value + b\"\\0\"\n\n    @_register_loader(5, 8)\n    def load_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}L\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(5)\n    def write_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2L\", *_limit_rational(frac, 2**32 - 1)) for frac in values\n        )\n\n    @_register_loader(7, 1)\n    def load_undefined(self, data, legacy_api=True):\n        return data\n\n    @_register_writer(7)\n    def write_undefined(self, value):\n        return value\n\n    @_register_loader(10, 8)\n    def load_signed_rational(self, data, legacy_api=True):\n        vals = self._unpack(f\"{len(data) // 4}l\", data)\n\n        def combine(a, b):\n            return (a, b) if legacy_api else IFDRational(a, b)\n\n        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))\n\n    @_register_writer(10)\n    def write_signed_rational(self, *values):\n        return b\"\".join(\n            self._pack(\"2l\", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))\n            for frac in values\n        )\n\n    def _ensure_read(self, fp, size):\n        ret = fp.read(size)\n        if len(ret) != size:\n            raise OSError(\n                \"Corrupt EXIF data.  \"\n                f\"Expecting to read {size} bytes but only got {len(ret)}. \"\n            )\n        return ret\n\n    def load(self, fp):\n\n        self.reset()\n        self._offset = fp.tell()\n\n        try:\n            tag_count = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"H\", self._ensure_read(fp, 2))\n            )[0]\n            for i in range(tag_count):\n                tag, typ, count, data = (\n                    self._unpack(\"HHQ8s\", self._ensure_read(fp, 20))\n                    if self._bigtiff\n                    else self._unpack(\"HHL4s\", self._ensure_read(fp, 12))\n                )\n\n                tagname = TiffTags.lookup(tag, self.group).name\n                typname = TYPES.get(typ, \"unknown\")\n                msg = f\"tag: {tagname} ({tag}) - type: {typname} ({typ})\"\n\n                try:\n                    unit_size, handler = self._load_dispatch[typ]\n                except KeyError:\n                    logger.debug(msg + f\" - unsupported type {typ}\")\n                    continue  # ignore unsupported type\n                size = count * unit_size\n                if size > (8 if self._bigtiff else 4):\n                    here = fp.tell()\n                    (offset,) = self._unpack(\"Q\" if self._bigtiff else \"L\", data)\n                    msg += f\" Tag Location: {here} - Data Location: {offset}\"\n                    fp.seek(offset)\n                    data = ImageFile._safe_read(fp, size)\n                    fp.seek(here)\n                else:\n                    data = data[:size]\n\n                if len(data) != size:\n                    warnings.warn(\n                        \"Possibly corrupt EXIF data.  \"\n                        f\"Expecting to read {size} bytes but only got {len(data)}.\"\n                        f\" Skipping tag {tag}\"\n                    )\n                    logger.debug(msg)\n                    continue\n\n                if not data:\n                    logger.debug(msg)\n                    continue\n\n                self._tagdata[tag] = data\n                self.tagtype[tag] = typ\n\n                msg += \" - value: \" + (\n                    \"<table: %d bytes>\" % size if size > 32 else repr(data)\n                )\n                logger.debug(msg)\n\n            (self.next,) = (\n                self._unpack(\"Q\", self._ensure_read(fp, 8))\n                if self._bigtiff\n                else self._unpack(\"L\", self._ensure_read(fp, 4))\n            )\n        except OSError as msg:\n            warnings.warn(str(msg))\n            return\n\n    def tobytes(self, offset=0):\n        # FIXME What about tagdata?\n        result = self._pack(\"H\", len(self._tags_v2))\n\n        entries = []\n        offset = offset + len(result) + len(self._tags_v2) * 12 + 4\n        stripoffsets = None\n\n        # pass 1: convert tags to binary format\n        # always write tags in ascending order\n        for tag, value in sorted(self._tags_v2.items()):\n            if tag == STRIPOFFSETS:\n                stripoffsets = len(entries)\n            typ = self.tagtype.get(tag)\n            logger.debug(f\"Tag {tag}, Type: {typ}, Value: {repr(value)}\")\n            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)\n            if is_ifd:\n                if self._endian == \"<\":\n                    ifh = b\"II\\x2A\\x00\\x08\\x00\\x00\\x00\"\n                else:\n                    ifh = b\"MM\\x00\\x2A\\x00\\x00\\x00\\x08\"\n                ifd = ImageFileDirectory_v2(ifh, group=tag)\n                values = self._tags_v2[tag]\n                for ifd_tag, ifd_value in values.items():\n                    ifd[ifd_tag] = ifd_value\n                data = ifd.tobytes(offset)\n            else:\n                values = value if isinstance(value, tuple) else (value,)\n                data = self._write_dispatch[typ](self, *values)\n\n            tagname = TiffTags.lookup(tag, self.group).name\n            typname = \"ifd\" if is_ifd else TYPES.get(typ, \"unknown\")\n            msg = f\"save: {tagname} ({tag}) - type: {typname} ({typ})\"\n            msg += \" - value: \" + (\n                \"<table: %d bytes>\" % len(data) if len(data) >= 16 else str(values)\n            )\n            logger.debug(msg)\n\n            # count is sum of lengths for string and arbitrary data\n            if is_ifd:\n                count = 1\n            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:\n                count = len(data)\n            else:\n                count = len(values)\n            # figure out if data fits into the entry\n            if len(data) <= 4:\n                entries.append((tag, typ, count, data.ljust(4, b\"\\0\"), b\"\"))\n            else:\n                entries.append((tag, typ, count, self._pack(\"L\", offset), data))\n                offset += (len(data) + 1) // 2 * 2  # pad to word\n\n        # update strip offset data to point beyond auxiliary data\n        if stripoffsets is not None:\n            tag, typ, count, value, data = entries[stripoffsets]\n            if data:\n                raise NotImplementedError(\"multistrip support not yet implemented\")\n            value = self._pack(\"L\", self._unpack(\"L\", value)[0] + offset)\n            entries[stripoffsets] = tag, typ, count, value, data\n\n        # pass 2: write entries to file\n        for tag, typ, count, value, data in entries:\n            logger.debug(f\"{tag} {typ} {count} {repr(value)} {repr(data)}\")\n            result += self._pack(\"HHL4s\", tag, typ, count, value)\n\n        # -- overwrite here for multi-page --\n        result += b\"\\0\\0\\0\\0\"  # end of entries\n\n        # pass 3: write auxiliary data to file\n        for tag, typ, count, value, data in entries:\n            result += data\n            if len(data) & 1:\n                result += b\"\\0\"\n\n        return result\n\n    def save(self, fp):\n\n        if fp.tell() == 0:  # skip TIFF header on subsequent pages\n            # tiff header -- PIL always starts the first IFD at offset 8\n            fp.write(self._prefix + self._pack(\"HL\", 42, 8))\n\n        offset = fp.tell()\n        result = self.tobytes(offset)\n        fp.write(result)\n        return offset + len(result)\n\n\nImageFileDirectory_v2._load_dispatch = _load_dispatch\nImageFileDirectory_v2._write_dispatch = _write_dispatch\nfor idx, name in TYPES.items():\n    name = name.replace(\" \", \"_\")\n    setattr(ImageFileDirectory_v2, \"load_\" + name, _load_dispatch[idx][1])\n    setattr(ImageFileDirectory_v2, \"write_\" + name, _write_dispatch[idx])\ndel _load_dispatch, _write_dispatch, idx, name\n\n\n# Legacy ImageFileDirectory support.\nclass ImageFileDirectory_v1(ImageFileDirectory_v2):\n    \"\"\"This class represents the **legacy** interface to a TIFF tag directory.\n\n    Exposes a dictionary interface of the tags in the directory::\n\n        ifd = ImageFileDirectory_v1()\n        ifd[key] = 'Some Data'\n        ifd.tagtype[key] = TiffTags.ASCII\n        print(ifd[key])\n        ('Some Data',)\n\n    Also contains a dictionary of tag types as read from the tiff image file,\n    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.\n\n    Values are returned as a tuple.\n\n    ..  deprecated:: 3.0.0\n    \"\"\"\n\n    def __init__(self, *args, **kwargs):\n        super().__init__(*args, **kwargs)\n        self._legacy_api = True\n\n    tags = property(lambda self: self._tags_v1)\n    tagdata = property(lambda self: self._tagdata)\n\n    # defined in ImageFileDirectory_v2\n    tagtype: dict\n    \"\"\"Dictionary of tag types\"\"\"\n\n    @classmethod\n    def from_v2(cls, original):\n        \"\"\"Returns an\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n        instance with the same data as is contained in the original\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n        instance.\n\n        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n\n        \"\"\"\n\n        ifd = cls(prefix=original.prefix)\n        ifd._tagdata = original._tagdata\n        ifd.tagtype = original.tagtype\n        ifd.next = original.next  # an indicator for multipage tiffs\n        return ifd\n\n    def to_v2(self):\n        \"\"\"Returns an\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n        instance with the same data as is contained in the original\n        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`\n        instance.\n\n        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`\n\n        \"\"\"\n\n        ifd = ImageFileDirectory_v2(prefix=self.prefix)\n        ifd._tagdata = dict(self._tagdata)\n        ifd.tagtype = dict(self.tagtype)\n        ifd._tags_v2 = dict(self._tags_v2)\n        return ifd\n\n    def __contains__(self, tag):\n        return tag in self._tags_v1 or tag in self._tagdata\n\n    def __len__(self):\n        return len(set(self._tagdata) | set(self._tags_v1))\n\n    def __iter__(self):\n        return iter(set(self._tagdata) | set(self._tags_v1))\n\n    def __setitem__(self, tag, value):\n        for legacy_api in (False, True):\n            self._setitem(tag, value, legacy_api)\n\n    def __getitem__(self, tag):\n        if tag not in self._tags_v1:  # unpack on the fly\n            data = self._tagdata[tag]\n            typ = self.tagtype[tag]\n            size, handler = self._load_dispatch[typ]\n            for legacy in (False, True):\n                self._setitem(tag, handler(self, data, legacy), legacy)\n        val = self._tags_v1[tag]\n        if not isinstance(val, (tuple, bytes)):\n            val = (val,)\n        return val\n\n\n# undone -- switch this pointer when IFD_LEGACY_API == False\nImageFileDirectory = ImageFileDirectory_v1\n\n\n##\n# Image plugin for TIFF files.\n\n\nclass TiffImageFile(ImageFile.ImageFile):\n\n    format = \"TIFF\"\n    format_description = \"Adobe TIFF\"\n    _close_exclusive_fp_after_loading = False\n\n    def __init__(self, fp=None, filename=None):\n        self.tag_v2 = None\n        \"\"\" Image file directory (tag dictionary) \"\"\"\n\n        self.tag = None\n        \"\"\" Legacy tag entries \"\"\"\n\n        super().__init__(fp, filename)\n\n    def _open(self):\n        \"\"\"Open the first image in a TIFF file\"\"\"\n\n        # Header\n        ifh = self.fp.read(8)\n        if ifh[2] == 43:\n            ifh += self.fp.read(8)\n\n        self.tag_v2 = ImageFileDirectory_v2(ifh)\n\n        # legacy IFD entries will be filled in later\n        self.ifd = None\n\n        # setup frame pointers\n        self.__first = self.__next = self.tag_v2.next\n        self.__frame = -1\n        self._fp = self.fp\n        self._frame_pos = []\n        self._n_frames = None\n\n        logger.debug(\"*** TiffImageFile._open ***\")\n        logger.debug(f\"- __first: {self.__first}\")\n        logger.debug(f\"- ifh: {repr(ifh)}\")  # Use repr to avoid str(bytes)\n\n        # and load the first frame\n        self._seek(0)\n\n    @property\n    def n_frames(self):\n        if self._n_frames is None:\n            current = self.tell()\n            self._seek(len(self._frame_pos))\n            while self._n_frames is None:\n                self._seek(self.tell() + 1)\n            self.seek(current)\n        return self._n_frames\n\n    def seek(self, frame):\n        \"\"\"Select a given frame as current image\"\"\"\n        if not self._seek_check(frame):\n            return\n        self._seek(frame)\n        # Create a new core image object on second and\n        # subsequent frames in the image. Image may be\n        # different size/mode.\n        Image._decompression_bomb_check(self.size)\n        self.im = Image.core.new(self.mode, self.size)\n\n    def _seek(self, frame):\n        self.fp = self._fp\n\n        # reset buffered io handle in case fp\n        # was passed to libtiff, invalidating the buffer\n        self.fp.tell()\n\n        while len(self._frame_pos) <= frame:\n            if not self.__next:\n                raise EOFError(\"no more images in TIFF file\")\n            logger.debug(\n                f\"Seeking to frame {frame}, on frame {self.__frame}, \"\n                f\"__next {self.__next}, location: {self.fp.tell()}\"\n            )\n            self.fp.seek(self.__next)\n            self._frame_pos.append(self.__next)\n            logger.debug(\"Loading tags, location: %s\" % self.fp.tell())\n            self.tag_v2.load(self.fp)\n            if self.tag_v2.next in self._frame_pos:\n                # This IFD has already been processed\n                # Declare this to be the end of the image\n                self.__next = 0\n            else:\n                self.__next = self.tag_v2.next\n            if self.__next == 0:\n                self._n_frames = frame + 1\n            if len(self._frame_pos) == 1:\n                self.is_animated = self.__next != 0\n            self.__frame += 1\n        self.fp.seek(self._frame_pos[frame])\n        self.tag_v2.load(self.fp)\n        self._reload_exif()\n        # fill the legacy tag/ifd entries\n        self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)\n        self.__frame = frame\n        self._setup()\n\n    def tell(self):\n        \"\"\"Return the current frame number\"\"\"\n        return self.__frame\n\n    def get_child_images(self):\n        if SUBIFD not in self.tag_v2:\n            return []\n        child_images = []\n        exif = self.getexif()\n        offset = None\n        for im_offset in self.tag_v2[SUBIFD]:\n            # reset buffered io handle in case fp\n            # was passed to libtiff, invalidating the buffer\n            current_offset = self._fp.tell()\n            if offset is None:\n                offset = current_offset\n\n            fp = self._fp\n            ifd = exif._get_ifd_dict(im_offset)\n            jpegInterchangeFormat = ifd.get(513)\n            if jpegInterchangeFormat is not None:\n                fp.seek(jpegInterchangeFormat)\n                jpeg_data = fp.read(ifd.get(514))\n\n                fp = io.BytesIO(jpeg_data)\n\n            with Image.open(fp) as im:\n                if jpegInterchangeFormat is None:\n                    im._frame_pos = [im_offset]\n                    im._seek(0)\n                im.load()\n                child_images.append(im)\n\n        if offset is not None:\n            self._fp.seek(offset)\n        return child_images\n\n    def getxmp(self):\n        \"\"\"\n        Returns a dictionary containing the XMP tags.\n        Requires defusedxml to be installed.\n\n        :returns: XMP tags in a dictionary.\n        \"\"\"\n        return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}\n\n    def get_photoshop_blocks(self):\n        \"\"\"\n        Returns a dictionary of Photoshop \"Image Resource Blocks\".\n        The keys are the image resource ID. For more information, see\n        https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727\n\n        :returns: Photoshop \"Image Resource Blocks\" in a dictionary.\n        \"\"\"\n        blocks = {}\n        val = self.tag_v2.get(0x8649)\n        if val:\n            while val[:4] == b\"8BIM\":\n                id = i16(val[4:6])\n                n = math.ceil((val[6] + 1) / 2) * 2\n                size = i32(val[6 + n : 10 + n])\n                data = val[10 + n : 10 + n + size]\n                blocks[id] = {\"data\": data}\n\n                val = val[math.ceil((10 + n + size) / 2) * 2 :]\n        return blocks\n\n    def load(self):\n        if self.tile and self.use_load_libtiff:\n            return self._load_libtiff()\n        return super().load()\n\n    def load_end(self):\n        if self._tile_orientation:\n            method = {\n                2: Image.Transpose.FLIP_LEFT_RIGHT,\n                3: Image.Transpose.ROTATE_180,\n                4: Image.Transpose.FLIP_TOP_BOTTOM,\n                5: Image.Transpose.TRANSPOSE,\n                6: Image.Transpose.ROTATE_270,\n                7: Image.Transpose.TRANSVERSE,\n                8: Image.Transpose.ROTATE_90,\n            }.get(self._tile_orientation)\n            if method is not None:\n                self.im = self.im.transpose(method)\n                self._size = self.im.size\n\n        # allow closing if we're on the first frame, there's no next\n        # This is the ImageFile.load path only, libtiff specific below.\n        if not self.is_animated:\n            self._close_exclusive_fp_after_loading = True\n\n            # reset buffered io handle in case fp\n            # was passed to libtiff, invalidating the buffer\n            self.fp.tell()\n\n            # load IFD data from fp before it is closed\n            exif = self.getexif()\n            for key in TiffTags.TAGS_V2_GROUPS.keys():\n                if key not in exif:\n                    continue\n                exif.get_ifd(key)\n\n    def _load_libtiff(self):\n        \"\"\"Overload method triggered when we detect a compressed tiff\n        Calls out to libtiff\"\"\"\n\n        Image.Image.load(self)\n\n        self.load_prepare()\n\n        if not len(self.tile) == 1:\n            raise OSError(\"Not exactly one tile\")\n\n        # (self._compression, (extents tuple),\n        #   0, (rawmode, self._compression, fp))\n        extents = self.tile[0][1]\n        args = list(self.tile[0][3])\n\n        # To be nice on memory footprint, if there's a\n        # file descriptor, use that instead of reading\n        # into a string in python.\n        # libtiff closes the file descriptor, so pass in a dup.\n        try:\n            fp = hasattr(self.fp, \"fileno\") and os.dup(self.fp.fileno())\n            # flush the file descriptor, prevents error on pypy 2.4+\n            # should also eliminate the need for fp.tell\n            # in _seek\n            if hasattr(self.fp, \"flush\"):\n                self.fp.flush()\n        except OSError:\n            # io.BytesIO have a fileno, but returns an OSError if\n            # it doesn't use a file descriptor.\n            fp = False\n\n        if fp:\n            args[2] = fp\n\n        decoder = Image._getdecoder(\n            self.mode, \"libtiff\", tuple(args), self.decoderconfig\n        )\n        try:\n            decoder.setimage(self.im, extents)\n        except ValueError as e:\n            raise OSError(\"Couldn't set the image\") from e\n\n        close_self_fp = self._exclusive_fp and not self.is_animated\n        if hasattr(self.fp, \"getvalue\"):\n            # We've got a stringio like thing passed in. Yay for all in memory.\n            # The decoder needs the entire file in one shot, so there's not\n            # a lot we can do here other than give it the entire file.\n            # unless we could do something like get the address of the\n            # underlying string for stringio.\n            #\n            # Rearranging for supporting byteio items, since they have a fileno\n            # that returns an OSError if there's no underlying fp. Easier to\n            # deal with here by reordering.\n            logger.debug(\"have getvalue. just sending in a string from getvalue\")\n            n, err = decoder.decode(self.fp.getvalue())\n        elif fp:\n            # we've got a actual file on disk, pass in the fp.\n            logger.debug(\"have fileno, calling fileno version of the decoder.\")\n            if not close_self_fp:\n                self.fp.seek(0)\n            # 4 bytes, otherwise the trace might error out\n            n, err = decoder.decode(b\"fpfp\")\n        else:\n            # we have something else.\n            logger.debug(\"don't have fileno or getvalue. just reading\")\n            self.fp.seek(0)\n            # UNDONE -- so much for that buffer size thing.\n            n, err = decoder.decode(self.fp.read())\n\n        if fp:\n            try:\n                os.close(fp)\n            except OSError:\n                pass\n\n        self.tile = []\n        self.readonly = 0\n\n        self.load_end()\n\n        # libtiff closed the fp in a, we need to close self.fp, if possible\n        if close_self_fp:\n            self.fp.close()\n            self.fp = None  # might be shared\n\n        if err < 0:\n            raise OSError(err)\n\n        return Image.Image.load(self)\n\n    def _setup(self):\n        \"\"\"Setup this image object based on current tags\"\"\"\n\n        if 0xBC01 in self.tag_v2:\n            raise OSError(\"Windows Media Photo files not yet supported\")\n\n        # extract relevant tags\n        self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]\n        self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)\n\n        # photometric is a required tag, but not everyone is reading\n        # the specification\n        photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)\n\n        # old style jpeg compression images most certainly are YCbCr\n        if self._compression == \"tiff_jpeg\":\n            photo = 6\n\n        fillorder = self.tag_v2.get(FILLORDER, 1)\n\n        logger.debug(\"*** Summary ***\")\n        logger.debug(f\"- compression: {self._compression}\")\n        logger.debug(f\"- photometric_interpretation: {photo}\")\n        logger.debug(f\"- planar_configuration: {self._planar_configuration}\")\n        logger.debug(f\"- fill_order: {fillorder}\")\n        logger.debug(f\"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}\")\n\n        # size\n        xsize = int(self.tag_v2.get(IMAGEWIDTH))\n        ysize = int(self.tag_v2.get(IMAGELENGTH))\n        self._size = xsize, ysize\n\n        logger.debug(f\"- size: {self.size}\")\n\n        sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))\n        if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:\n            # SAMPLEFORMAT is properly per band, so an RGB image will\n            # be (1,1,1).  But, we don't support per band pixel types,\n            # and anything more than one band is a uint8. So, just\n            # take the first element. Revisit this if adding support\n            # for more exotic images.\n            sample_format = (1,)\n\n        bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))\n        extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())\n        if photo in (2, 6, 8):  # RGB, YCbCr, LAB\n            bps_count = 3\n        elif photo == 5:  # CMYK\n            bps_count = 4\n        else:\n            bps_count = 1\n        bps_count += len(extra_tuple)\n        bps_actual_count = len(bps_tuple)\n        samples_per_pixel = self.tag_v2.get(\n            SAMPLESPERPIXEL,\n            3 if self._compression == \"tiff_jpeg\" and photo in (2, 6) else 1,\n        )\n\n        if samples_per_pixel > MAX_SAMPLESPERPIXEL:\n            # DOS check, samples_per_pixel can be a Long, and we extend the tuple below\n            logger.error(\n                \"More samples per pixel than can be decoded: %s\", samples_per_pixel\n            )\n            raise SyntaxError(\"Invalid value for samples per pixel\")\n\n        if samples_per_pixel < bps_actual_count:\n            # If a file has more values in bps_tuple than expected,\n            # remove the excess.\n            bps_tuple = bps_tuple[:samples_per_pixel]\n        elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:\n            # If a file has only one value in bps_tuple, when it should have more,\n            # presume it is the same number of bits for all of the samples.\n            bps_tuple = bps_tuple * samples_per_pixel\n\n        if len(bps_tuple) != samples_per_pixel:\n            raise SyntaxError(\"unknown data organization\")\n\n        # mode: check photometric interpretation and bits per pixel\n        key = (\n            self.tag_v2.prefix,\n            photo,\n            sample_format,\n            fillorder,\n            bps_tuple,\n            extra_tuple,\n        )\n        logger.debug(f\"format key: {key}\")\n        try:\n            self.mode, rawmode = OPEN_INFO[key]\n        except KeyError as e:\n            logger.debug(\"- unsupported format\")\n            raise SyntaxError(\"unknown pixel mode\") from e\n\n        logger.debug(f\"- raw mode: {rawmode}\")\n        logger.debug(f\"- pil mode: {self.mode}\")\n\n        self.info[\"compression\"] = self._compression\n\n        xres = self.tag_v2.get(X_RESOLUTION, 1)\n        yres = self.tag_v2.get(Y_RESOLUTION, 1)\n\n        if xres and yres:\n            resunit = self.tag_v2.get(RESOLUTION_UNIT)\n            if resunit == 2:  # dots per inch\n                self.info[\"dpi\"] = (xres, yres)\n            elif resunit == 3:  # dots per centimeter. convert to dpi\n                self.info[\"dpi\"] = (xres * 2.54, yres * 2.54)\n            elif resunit is None:  # used to default to 1, but now 2)\n                self.info[\"dpi\"] = (xres, yres)\n                # For backward compatibility,\n                # we also preserve the old behavior\n                self.info[\"resolution\"] = xres, yres\n            else:  # No absolute unit of measurement\n                self.info[\"resolution\"] = xres, yres\n\n        # build tile descriptors\n        x = y = layer = 0\n        self.tile = []\n        self.use_load_libtiff = READ_LIBTIFF or self._compression != \"raw\"\n        if self.use_load_libtiff:\n            # Decoder expects entire file as one tile.\n            # There's a buffer size limit in load (64k)\n            # so large g4 images will fail if we use that\n            # function.\n            #\n            # Setup the one tile for the whole image, then\n            # use the _load_libtiff function.\n\n            # libtiff handles the fillmode for us, so 1;IR should\n            # actually be 1;I. Including the R double reverses the\n            # bits, so stripes of the image are reversed.  See\n            # https://github.com/python-pillow/Pillow/issues/279\n            if fillorder == 2:\n                # Replace fillorder with fillorder=1\n                key = key[:3] + (1,) + key[4:]\n                logger.debug(f\"format key: {key}\")\n                # this should always work, since all the\n                # fillorder==2 modes have a corresponding\n                # fillorder=1 mode\n                self.mode, rawmode = OPEN_INFO[key]\n            # libtiff always returns the bytes in native order.\n            # we're expecting image byte order. So, if the rawmode\n            # contains I;16, we need to convert from native to image\n            # byte order.\n            if rawmode == \"I;16\":\n                rawmode = \"I;16N\"\n            if \";16B\" in rawmode:\n                rawmode = rawmode.replace(\";16B\", \";16N\")\n            if \";16L\" in rawmode:\n                rawmode = rawmode.replace(\";16L\", \";16N\")\n\n            # YCbCr images with new jpeg compression with pixels in one plane\n            # unpacked straight into RGB values\n            if (\n                photo == 6\n                and self._compression == \"jpeg\"\n                and self._planar_configuration == 1\n            ):\n                rawmode = \"RGB\"\n\n            # Offset in the tile tuple is 0, we go from 0,0 to\n            # w,h, and we only do this once -- eds\n            a = (rawmode, self._compression, False, self.tag_v2.offset)\n            self.tile.append((\"libtiff\", (0, 0, xsize, ysize), 0, a))\n\n        elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:\n            # striped image\n            if STRIPOFFSETS in self.tag_v2:\n                offsets = self.tag_v2[STRIPOFFSETS]\n                h = self.tag_v2.get(ROWSPERSTRIP, ysize)\n                w = self.size[0]\n            else:\n                # tiled image\n                offsets = self.tag_v2[TILEOFFSETS]\n                w = self.tag_v2.get(TILEWIDTH)\n                h = self.tag_v2.get(TILELENGTH)\n\n            for offset in offsets:\n                if x + w > xsize:\n                    stride = w * sum(bps_tuple) / 8  # bytes per line\n                else:\n                    stride = 0\n\n                tile_rawmode = rawmode\n                if self._planar_configuration == 2:\n                    # each band on it's own layer\n                    tile_rawmode = rawmode[layer]\n                    # adjust stride width accordingly\n                    stride /= bps_count\n\n                a = (tile_rawmode, int(stride), 1)\n                self.tile.append(\n                    (\n                        self._compression,\n                        (x, y, min(x + w, xsize), min(y + h, ysize)),\n                        offset,\n                        a,\n                    )\n                )\n                x = x + w\n                if x >= self.size[0]:\n                    x, y = 0, y + h\n                    if y >= self.size[1]:\n                        x = y = 0\n                        layer += 1\n        else:\n            logger.debug(\"- unsupported data organization\")\n            raise SyntaxError(\"unknown data organization\")\n\n        # Fix up info.\n        if ICCPROFILE in self.tag_v2:\n            self.info[\"icc_profile\"] = self.tag_v2[ICCPROFILE]\n\n        # fixup palette descriptor\n\n        if self.mode in [\"P\", \"PA\"]:\n            palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]\n            self.palette = ImagePalette.raw(\"RGB;L\", b\"\".join(palette))\n\n        self._tile_orientation = self.tag_v2.get(0x0112)\n\n\n#\n# --------------------------------------------------------------------\n# Write TIFF files\n\n# little endian is default except for image modes with\n# explicit big endian byte-order\n\nSAVE_INFO = {\n    # mode => rawmode, byteorder, photometrics,\n    #           sampleformat, bitspersample, extra\n    \"1\": (\"1\", II, 1, 1, (1,), None),\n    \"L\": (\"L\", II, 1, 1, (8,), None),\n    \"LA\": (\"LA\", II, 1, 1, (8, 8), 2),\n    \"P\": (\"P\", II, 3, 1, (8,), None),\n    \"PA\": (\"PA\", II, 3, 1, (8, 8), 2),\n    \"I\": (\"I;32S\", II, 1, 2, (32,), None),\n    \"I;16\": (\"I;16\", II, 1, 1, (16,), None),\n    \"I;16S\": (\"I;16S\", II, 1, 2, (16,), None),\n    \"F\": (\"F;32F\", II, 1, 3, (32,), None),\n    \"RGB\": (\"RGB\", II, 2, 1, (8, 8, 8), None),\n    \"RGBX\": (\"RGBX\", II, 2, 1, (8, 8, 8, 8), 0),\n    \"RGBA\": (\"RGBA\", II, 2, 1, (8, 8, 8, 8), 2),\n    \"CMYK\": (\"CMYK\", II, 5, 1, (8, 8, 8, 8), None),\n    \"YCbCr\": (\"YCbCr\", II, 6, 1, (8, 8, 8), None),\n    \"LAB\": (\"LAB\", II, 8, 1, (8, 8, 8), None),\n    \"I;32BS\": (\"I;32BS\", MM, 1, 2, (32,), None),\n    \"I;16B\": (\"I;16B\", MM, 1, 1, (16,), None),\n    \"I;16BS\": (\"I;16BS\", MM, 1, 2, (16,), None),\n    \"F;32BF\": (\"F;32BF\", MM, 1, 3, (32,), None),\n}\n\n\ndef _save(im, fp, filename):\n\n    try:\n        rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]\n    except KeyError as e:\n        raise OSError(f\"cannot write mode {im.mode} as TIFF\") from e\n\n    ifd = ImageFileDirectory_v2(prefix=prefix)\n\n    encoderinfo = im.encoderinfo\n    encoderconfig = im.encoderconfig\n    try:\n        compression = encoderinfo[\"compression\"]\n    except KeyError:\n        compression = im.info.get(\"compression\")\n        if isinstance(compression, int):\n            # compression value may be from BMP. Ignore it\n            compression = None\n    if compression is None:\n        compression = \"raw\"\n    elif compression == \"tiff_jpeg\":\n        # OJPEG is obsolete, so use new-style JPEG compression instead\n        compression = \"jpeg\"\n    elif compression == \"tiff_deflate\":\n        compression = \"tiff_adobe_deflate\"\n\n    libtiff = WRITE_LIBTIFF or compression != \"raw\"\n\n    # required for color libtiff images\n    ifd[PLANAR_CONFIGURATION] = 1\n\n    ifd[IMAGEWIDTH] = im.size[0]\n    ifd[IMAGELENGTH] = im.size[1]\n\n    # write any arbitrary tags passed in as an ImageFileDirectory\n    if \"tiffinfo\" in encoderinfo:\n        info = encoderinfo[\"tiffinfo\"]\n    elif \"exif\" in encoderinfo:\n        info = encoderinfo[\"exif\"]\n        if isinstance(info, bytes):\n            exif = Image.Exif()\n            exif.load(info)\n            info = exif\n    else:\n        info = {}\n    logger.debug(\"Tiffinfo Keys: %s\" % list(info))\n    if isinstance(info, ImageFileDirectory_v1):\n        info = info.to_v2()\n    for key in info:\n        if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():\n            ifd[key] = info.get_ifd(key)\n        else:\n            ifd[key] = info.get(key)\n        try:\n            ifd.tagtype[key] = info.tagtype[key]\n        except Exception:\n            pass  # might not be an IFD. Might not have populated type\n\n    # additions written by Greg Couch, gregc@cgl.ucsf.edu\n    # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com\n    if hasattr(im, \"tag_v2\"):\n        # preserve tags from original TIFF image file\n        for key in (\n            RESOLUTION_UNIT,\n            X_RESOLUTION,\n            Y_RESOLUTION,\n            IPTC_NAA_CHUNK,\n            PHOTOSHOP_CHUNK,\n            XMP,\n        ):\n            if key in im.tag_v2:\n                ifd[key] = im.tag_v2[key]\n                ifd.tagtype[key] = im.tag_v2.tagtype[key]\n\n    # preserve ICC profile (should also work when saving other formats\n    # which support profiles as TIFF) -- 2008-06-06 Florian Hoech\n    icc = encoderinfo.get(\"icc_profile\", im.info.get(\"icc_profile\"))\n    if icc:\n        ifd[ICCPROFILE] = icc\n\n    for key, name in [\n        (IMAGEDESCRIPTION, \"description\"),\n        (X_RESOLUTION, \"resolution\"),\n        (Y_RESOLUTION, \"resolution\"),\n        (X_RESOLUTION, \"x_resolution\"),\n        (Y_RESOLUTION, \"y_resolution\"),\n        (RESOLUTION_UNIT, \"resolution_unit\"),\n        (SOFTWARE, \"software\"),\n        (DATE_TIME, \"date_time\"),\n        (ARTIST, \"artist\"),\n        (COPYRIGHT, \"copyright\"),\n    ]:\n        if name in encoderinfo:\n            ifd[key] = encoderinfo[name]\n\n    dpi = encoderinfo.get(\"dpi\")\n    if dpi:\n        ifd[RESOLUTION_UNIT] = 2\n        ifd[X_RESOLUTION] = dpi[0]\n        ifd[Y_RESOLUTION] = dpi[1]\n\n    if bits != (1,):\n        ifd[BITSPERSAMPLE] = bits\n        if len(bits) != 1:\n            ifd[SAMPLESPERPIXEL] = len(bits)\n    if extra is not None:\n        ifd[EXTRASAMPLES] = extra\n    if format != 1:\n        ifd[SAMPLEFORMAT] = format\n\n    if PHOTOMETRIC_INTERPRETATION not in ifd:\n        ifd[PHOTOMETRIC_INTERPRETATION] = photo\n    elif im.mode in (\"1\", \"L\") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:\n        if im.mode == \"1\":\n            inverted_im = im.copy()\n            px = inverted_im.load()\n            for y in range(inverted_im.height):\n                for x in range(inverted_im.width):\n                    px[x, y] = 0 if px[x, y] == 255 else 255\n            im = inverted_im\n        else:\n            im = ImageOps.invert(im)\n\n    if im.mode in [\"P\", \"PA\"]:\n        lut = im.im.getpalette(\"RGB\", \"RGB;L\")\n        colormap = []\n        colors = len(lut) // 3\n        for i in range(3):\n            colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]\n            colormap += [0] * (256 - colors)\n        ifd[COLORMAP] = colormap\n    # data orientation\n    stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)\n    # aim for given strip size (64 KB by default) when using libtiff writer\n    if libtiff:\n        im_strip_size = encoderinfo.get(\"strip_size\", STRIP_SIZE)\n        rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])\n        # JPEG encoder expects multiple of 8 rows\n        if compression == \"jpeg\":\n            rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])\n    else:\n        rows_per_strip = im.size[1]\n    if rows_per_strip == 0:\n        rows_per_strip = 1\n    strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip\n    strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip\n    ifd[ROWSPERSTRIP] = rows_per_strip\n    if strip_byte_counts >= 2**16:\n        ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG\n    ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (\n        stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),\n    )\n    ifd[STRIPOFFSETS] = tuple(\n        range(0, strip_byte_counts * strips_per_image, strip_byte_counts)\n    )  # this is adjusted by IFD writer\n    # no compression by default:\n    ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)\n\n    if im.mode == \"YCbCr\":\n        for tag, value in {\n            YCBCRSUBSAMPLING: (1, 1),\n            REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),\n        }.items():\n            ifd.setdefault(tag, value)\n\n    blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]\n    if libtiff:\n        if \"quality\" in encoderinfo:\n            quality = encoderinfo[\"quality\"]\n            if not isinstance(quality, int) or quality < 0 or quality > 100:\n                raise ValueError(\"Invalid quality setting\")\n            if compression != \"jpeg\":\n                raise ValueError(\n                    \"quality setting only supported for 'jpeg' compression\"\n                )\n            ifd[JPEGQUALITY] = quality\n\n        logger.debug(\"Saving using libtiff encoder\")\n        logger.debug(\"Items: %s\" % sorted(ifd.items()))\n        _fp = 0\n        if hasattr(fp, \"fileno\"):\n            try:\n                fp.seek(0)\n                _fp = os.dup(fp.fileno())\n            except io.UnsupportedOperation:\n                pass\n\n        # optional types for non core tags\n        types = {}\n        # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library\n        # based on the data in the strip.\n        # The other tags expect arrays with a certain length (fixed or depending on\n        # BITSPERSAMPLE, etc), passing arrays with a different length will result in\n        # segfaults. Block these tags until we add extra validation.\n        # SUBIFD may also cause a segfault.\n        blocklist += [\n            REFERENCEBLACKWHITE,\n            STRIPBYTECOUNTS,\n            STRIPOFFSETS,\n            TRANSFERFUNCTION,\n            SUBIFD,\n        ]\n\n        # bits per sample is a single short in the tiff directory, not a list.\n        atts = {BITSPERSAMPLE: bits[0]}\n        # Merge the ones that we have with (optional) more bits from\n        # the original file, e.g x,y resolution so that we can\n        # save(load('')) == original file.\n        legacy_ifd = {}\n        if hasattr(im, \"tag\"):\n            legacy_ifd = im.tag.to_v2()\n\n        # SAMPLEFORMAT is determined by the image format and should not be copied\n        # from legacy_ifd.\n        supplied_tags = {**getattr(im, \"tag_v2\", {}), **legacy_ifd}\n        if SAMPLEFORMAT in supplied_tags:\n            del supplied_tags[SAMPLEFORMAT]\n\n        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):\n            # Libtiff can only process certain core items without adding\n            # them to the custom dictionary.\n            # Custom items are supported for int, float, unicode, string and byte\n            # values. Other types and tuples require a tagtype.\n            if tag not in TiffTags.LIBTIFF_CORE:\n                if not Image.core.libtiff_support_custom_tags:\n                    continue\n\n                if tag in ifd.tagtype:\n                    types[tag] = ifd.tagtype[tag]\n                elif not (isinstance(value, (int, float, str, bytes))):\n                    continue\n                else:\n                    type = TiffTags.lookup(tag).type\n                    if type:\n                        types[tag] = type\n            if tag not in atts and tag not in blocklist:\n                if isinstance(value, str):\n                    atts[tag] = value.encode(\"ascii\", \"replace\") + b\"\\0\"\n                elif isinstance(value, IFDRational):\n                    atts[tag] = float(value)\n                else:\n                    atts[tag] = value\n\n        if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:\n            atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]\n\n        logger.debug(\"Converted items: %s\" % sorted(atts.items()))\n\n        # libtiff always expects the bytes in native order.\n        # we're storing image byte order. So, if the rawmode\n        # contains I;16, we need to convert from native to image\n        # byte order.\n        if im.mode in (\"I;16B\", \"I;16\"):\n            rawmode = \"I;16N\"\n\n        # Pass tags as sorted list so that the tags are set in a fixed order.\n        # This is required by libtiff for some tags. For example, the JPEGQUALITY\n        # pseudo tag requires that the COMPRESS tag was already set.\n        tags = list(atts.items())\n        tags.sort()\n        a = (rawmode, compression, _fp, filename, tags, types)\n        e = Image._getencoder(im.mode, \"libtiff\", a, encoderconfig)\n        e.setimage(im.im, (0, 0) + im.size)\n        while True:\n            # undone, change to self.decodermaxblock:\n            l, s, d = e.encode(16 * 1024)\n            if not _fp:\n                fp.write(d)\n            if s:\n                break\n        if s < 0:\n            raise OSError(f\"encoder error {s} when writing image file\")\n\n    else:\n        for tag in blocklist:\n            del ifd[tag]\n        offset = ifd.save(fp)\n\n        ImageFile._save(\n            im, fp, [(\"raw\", (0, 0) + im.size, offset, (rawmode, stride, 1))]\n        )\n\n    # -- helper for multi-page save --\n    if \"_debug_multipage\" in encoderinfo:\n        # just to access o32 and o16 (using correct byte order)\n        im._debug_multipage = ifd\n\n\nclass AppendingTiffWriter:\n    fieldSizes = [\n        0,  # None\n        1,  # byte\n        1,  # ascii\n        2,  # short\n        4,  # long\n        8,  # rational\n        1,  # sbyte\n        1,  # undefined\n        2,  # sshort\n        4,  # slong\n        8,  # srational\n        4,  # float\n        8,  # double\n    ]\n\n    #    StripOffsets = 273\n    #    FreeOffsets = 288\n    #    TileOffsets = 324\n    #    JPEGQTables = 519\n    #    JPEGDCTables = 520\n    #    JPEGACTables = 521\n    Tags = {273, 288, 324, 519, 520, 521}\n\n    def __init__(self, fn, new=False):\n        if hasattr(fn, \"read\"):\n            self.f = fn\n            self.close_fp = False\n        else:\n            self.name = fn\n            self.close_fp = True\n            try:\n                self.f = open(fn, \"w+b\" if new else \"r+b\")\n            except OSError:\n                self.f = open(fn, \"w+b\")\n        self.beginning = self.f.tell()\n        self.setup()\n\n    def setup(self):\n        # Reset everything.\n        self.f.seek(self.beginning, os.SEEK_SET)\n\n        self.whereToWriteNewIFDOffset = None\n        self.offsetOfNewPage = 0\n\n        self.IIMM = iimm = self.f.read(4)\n        if not iimm:\n            # empty file - first page\n            self.isFirst = True\n            return\n\n        self.isFirst = False\n        if iimm == b\"II\\x2a\\x00\":\n            self.setEndian(\"<\")\n        elif iimm == b\"MM\\x00\\x2a\":\n            self.setEndian(\">\")\n        else:\n            raise RuntimeError(\"Invalid TIFF file header\")\n\n        self.skipIFDs()\n        self.goToEnd()\n\n    def finalize(self):\n        if self.isFirst:\n            return\n\n        # fix offsets\n        self.f.seek(self.offsetOfNewPage)\n\n        iimm = self.f.read(4)\n        if not iimm:\n            # raise RuntimeError(\"nothing written into new page\")\n            # Make it easy to finish a frame without committing to a new one.\n            return\n\n        if iimm != self.IIMM:\n            raise RuntimeError(\"IIMM of new page doesn't match IIMM of first page\")\n\n        ifd_offset = self.readLong()\n        ifd_offset += self.offsetOfNewPage\n        self.f.seek(self.whereToWriteNewIFDOffset)\n        self.writeLong(ifd_offset)\n        self.f.seek(ifd_offset)\n        self.fixIFD()\n\n    def newFrame(self):\n        # Call this to finish a frame.\n        self.finalize()\n        self.setup()\n\n    def __enter__(self):\n        return self\n\n    def __exit__(self, exc_type, exc_value, traceback):\n        if self.close_fp:\n            self.close()\n        return False\n\n    def tell(self):\n        return self.f.tell() - self.offsetOfNewPage\n\n    def seek(self, offset, whence=io.SEEK_SET):\n        if whence == os.SEEK_SET:\n            offset += self.offsetOfNewPage\n\n        self.f.seek(offset, whence)\n        return self.tell()\n\n    def goToEnd(self):\n        self.f.seek(0, os.SEEK_END)\n        pos = self.f.tell()\n\n        # pad to 16 byte boundary\n        pad_bytes = 16 - pos % 16\n        if 0 < pad_bytes < 16:\n            self.f.write(bytes(pad_bytes))\n        self.offsetOfNewPage = self.f.tell()\n\n    def setEndian(self, endian):\n        self.endian = endian\n        self.longFmt = self.endian + \"L\"\n        self.shortFmt = self.endian + \"H\"\n        self.tagFormat = self.endian + \"HHL\"\n\n    def skipIFDs(self):\n        while True:\n            ifd_offset = self.readLong()\n            if ifd_offset == 0:\n                self.whereToWriteNewIFDOffset = self.f.tell() - 4\n                break\n\n            self.f.seek(ifd_offset)\n            num_tags = self.readShort()\n            self.f.seek(num_tags * 12, os.SEEK_CUR)\n\n    def write(self, data):\n        return self.f.write(data)\n\n    def readShort(self):\n        (value,) = struct.unpack(self.shortFmt, self.f.read(2))\n        return value\n\n    def readLong(self):\n        (value,) = struct.unpack(self.longFmt, self.f.read(4))\n        return value\n\n    def rewriteLastShortToLong(self, value):\n        self.f.seek(-2, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def rewriteLastShort(self, value):\n        self.f.seek(-2, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.shortFmt, value))\n        if bytes_written is not None and bytes_written != 2:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 2\")\n\n    def rewriteLastLong(self, value):\n        self.f.seek(-4, os.SEEK_CUR)\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def writeShort(self, value):\n        bytes_written = self.f.write(struct.pack(self.shortFmt, value))\n        if bytes_written is not None and bytes_written != 2:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 2\")\n\n    def writeLong(self, value):\n        bytes_written = self.f.write(struct.pack(self.longFmt, value))\n        if bytes_written is not None and bytes_written != 4:\n            raise RuntimeError(f\"wrote only {bytes_written} bytes but wanted 4\")\n\n    def close(self):\n        self.finalize()\n        self.f.close()\n\n    def fixIFD(self):\n        num_tags = self.readShort()\n\n        for i in range(num_tags):\n            tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))\n\n            field_size = self.fieldSizes[field_type]\n            total_size = field_size * count\n            is_local = total_size <= 4\n            if not is_local:\n                offset = self.readLong()\n                offset += self.offsetOfNewPage\n                self.rewriteLastLong(offset)\n\n            if tag in self.Tags:\n                cur_pos = self.f.tell()\n\n                if is_local:\n                    self.fixOffsets(\n                        count, isShort=(field_size == 2), isLong=(field_size == 4)\n                    )\n                    self.f.seek(cur_pos + 4)\n                else:\n                    self.f.seek(offset)\n                    self.fixOffsets(\n                        count, isShort=(field_size == 2), isLong=(field_size == 4)\n                    )\n                    self.f.seek(cur_pos)\n\n                offset = cur_pos = None\n\n            elif is_local:\n                # skip the locally stored value that is not an offset\n                self.f.seek(4, os.SEEK_CUR)\n\n    def fixOffsets(self, count, isShort=False, isLong=False):\n        if not isShort and not isLong:\n            raise RuntimeError(\"offset is neither short nor long\")\n\n        for i in range(count):\n            offset = self.readShort() if isShort else self.readLong()\n            offset += self.offsetOfNewPage\n            if isShort and offset >= 65536:\n                # offset is now too large - we must convert shorts to longs\n                if count != 1:\n                    raise RuntimeError(\"not implemented\")  # XXX TODO\n\n                # simple case - the offset is just one and therefore it is\n                # local (not referenced with another offset)\n                self.rewriteLastShortToLong(offset)\n                self.f.seek(-10, os.SEEK_CUR)\n                self.writeShort(TiffTags.LONG)  # rewrite the type to LONG\n                self.f.seek(8, os.SEEK_CUR)\n            elif isShort:\n                self.rewriteLastShort(offset)\n            else:\n                self.rewriteLastLong(offset)\n\n\ndef _save_all(im, fp, filename):\n    encoderinfo = im.encoderinfo.copy()\n    encoderconfig = im.encoderconfig\n    append_images = list(encoderinfo.get(\"append_images\", []))\n    if not hasattr(im, \"n_frames\") and not append_images:\n        return _save(im, fp, filename)\n\n    cur_idx = im.tell()\n    try:\n        with AppendingTiffWriter(fp) as tf:\n            for ims in [im] + append_images:\n                ims.encoderinfo = encoderinfo\n                ims.encoderconfig = encoderconfig\n                if not hasattr(ims, \"n_frames\"):\n                    nfr = 1\n                else:\n                    nfr = ims.n_frames\n\n                for idx in range(nfr):\n                    ims.seek(idx)\n                    ims.load()\n                    _save(ims, tf, filename)\n                    tf.newFrame()\n    finally:\n        im.seek(cur_idx)\n\n\n#\n# --------------------------------------------------------------------\n# Register\n\nImage.register_open(TiffImageFile.format, TiffImageFile, _accept)\nImage.register_save(TiffImageFile.format, _save)\nImage.register_save_all(TiffImageFile.format, _save_all)\n\nImage.register_extensions(TiffImageFile.format, [\".tif\", \".tiff\"])\n\nImage.register_mime(TiffImageFile.format, \"image/tiff\")\n"}
A user will ask you to solve a task. You should first draft your thinking process (inner monologue). Then, generate the solution.

Your response format must follow the template below:
<think>
Your thoughts or/and draft, like working through an exercise on scratch paper. Be as casual and as long as you want until you are confident to generate a correct solution.
</think>
<solution>
Final solution presented to the user.
</solution>
system
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Writing integer values as ASCII tags raises AttributeError

**Description**
When assigning an integer to an ASCII tag in TIFF metadata and attempting to save the image, an `AttributeError` is triggered because integers do not have an `encode` method. This issue prevents users from using integer values for ASCII tags, which can be necessary for certain metadata specifications.

**Example Code**
```python
from PIL import Image, TiffImagePlugin, TiffTags

# Create a new image
im = Image.new("RGB", (100, 100))
info = TiffImagePlugin.ImageFileDirectory_v2()

# Assign an integer to an ASCII tag
tag = TiffTags.TAGS_V2[271]  # Example tag for ImageDescription
info[271] = 123  # Integer value

# Attempt to save the image with the TIFF metadata
out = "temp.tiff"
im.save(out, tiffinfo=info)
```

**Expected Behavior**
The integer value should be automatically converted to a string and saved as an ASCII tag in the TIFF metadata without any errors.

**Actual Behavior**
An `AttributeError` is raised with the message: `'int' object has no attribute 'encode'`.

[/ISSUE]
--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.

--- BEGIN FILE ---
```
### src/PIL/TiffImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# TIFF file handling
#
# TIFF is a flexible, if somewhat aged, image file format originally
# defined by Aldus.  Although TIFF supports a wide variety of pixel
# layouts and compression methods, the name doesn't really stand for
# "thousands of incompatible file formats," it just feels that way.
#
# To read TIFF data from a stream, the stream must be seekable.  For
# progressive decoding, make sure to use TIFF files where the tag
# directory is placed first in the file.
#
# History:
# 1995-09-01 fl   Created
# 1996-05-04 fl   Handle JPEGTABLES tag
# 1996-05-18 fl   Fixed COLORMAP support
# 1997-01-05 fl   Fixed PREDICTOR support
# 1997-08-27 fl   Added support for rational tags (from Perry Stoll)
# 1998-01-10 fl   Fixed seek/tell (from Jan Blom)
# 1998-07-15 fl   Use private names for internal variables
# 1999-06-13 fl   Rewritten for PIL 1.0 (1.0)
# 2000-10-11 fl   Additional fixes for Python 2.0 (1.1)
# 2001-04-17 fl   Fixed rewind support (seek to frame 0) (1.2)
# 2001-05-12 fl   Added write support for more tags (from Greg Couch) (1.3)
# 2001-12-18 fl   Added workaround for broken Matrox library
# 2002-01-18 fl   Don't mess up if photometric tag is missing (D. Alan Stewart)
# 2003-05-19 fl   Check FILLORDER tag
# 2003-09-26 fl   Added RGBa support
# 2004-02-24 fl   Added DPI support; fixed rational write support
# 2005-02-07 fl   Added workaround for broken Corel Draw 10 files
# 2006-01-09 fl   Added support for float/double tags (from Russell Nelson)
#
# Copyright (c) 1997-2006 by Secret Labs AB.  All rights reserved.
# Copyright (c) 1995-1997 by Fredrik Lundh
#
# See the README file for information on usage and redistribution.
#
import io
import itertools
import logging
import math
import os
import struct
import warnings
from collections.abc import MutableMapping
from fractions import Fraction
from numbers import Number, Rational

from . import Image, ImageFile, ImageOps, ImagePalette, TiffTags
from ._binary import i16be as i16
from ._binary import i32be as i32
from ._binary import o8
from .TiffTags import TYPES

logger = logging.getLogger(__name__)

# Set these to true to force use of libtiff for reading or writing.
READ_LIBTIFF = False
WRITE_LIBTIFF = False
IFD_LEGACY_API = True
STRIP_SIZE = 65536

II = b"II"  # little-endian (Intel style)
MM = b"MM"  # big-endian (Motorola style)

#
# --------------------------------------------------------------------
# Read TIFF files

# a few tag names, just to make the code below a bit more readable
IMAGEWIDTH = 256
IMAGELENGTH = 257
BITSPERSAMPLE = 258
COMPRESSION = 259
PHOTOMETRIC_INTERPRETATION = 262
FILLORDER = 266
IMAGEDESCRIPTION = 270
STRIPOFFSETS = 273
SAMPLESPERPIXEL = 277
ROWSPERSTRIP = 278
STRIPBYTECOUNTS = 279
X_RESOLUTION = 282
Y_RESOLUTION = 283
PLANAR_CONFIGURATION = 284
RESOLUTION_UNIT = 296
TRANSFERFUNCTION = 301
SOFTWARE = 305
DATE_TIME = 306
ARTIST = 315
PREDICTOR = 317
COLORMAP = 320
TILEWIDTH = 322
TILELENGTH = 323
TILEOFFSETS = 324
TILEBYTECOUNTS = 325
SUBIFD = 330
EXTRASAMPLES = 338
SAMPLEFORMAT = 339
JPEGTABLES = 347
YCBCRSUBSAMPLING = 530
REFERENCEBLACKWHITE = 532
COPYRIGHT = 33432
IPTC_NAA_CHUNK = 33723  # newsphoto properties
PHOTOSHOP_CHUNK = 34377  # photoshop properties
ICCPROFILE = 34675
EXIFIFD = 34665
XMP = 700
JPEGQUALITY = 65537  # pseudo-tag by libtiff

# https://github.com/imagej/ImageJA/blob/master/src/main/java/ij/io/TiffDecoder.java
IMAGEJ_META_DATA_BYTE_COUNTS = 50838
IMAGEJ_META_DATA = 50839

COMPRESSION_INFO = {
    # Compression => pil compression name
    1: "raw",
    2: "tiff_ccitt",
    3: "group3",
    4: "group4",
    5: "tiff_lzw",
    6: "tiff_jpeg",  # obsolete
    7: "jpeg",
    8: "tiff_adobe_deflate",
    32771: "tiff_raw_16",  # 16-bit padding
    32773: "packbits",
    32809: "tiff_thunderscan",
    32946: "tiff_deflate",
    34676: "tiff_sgilog",
    34677: "tiff_sgilog24",
    34925: "lzma",
    50000: "zstd",
    50001: "webp",
}

COMPRESSION_INFO_REV = {v: k for k, v in COMPRESSION_INFO.items()}

OPEN_INFO = {
    # (ByteOrder, PhotoInterpretation, SampleFormat, FillOrder, BitsPerSample,
    #  ExtraSamples) => mode, rawmode
    (II, 0, (1,), 1, (1,), ()): ("1", "1;I"),
    (MM, 0, (1,), 1, (1,), ()): ("1", "1;I"),
    (II, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
    (MM, 0, (1,), 2, (1,), ()): ("1", "1;IR"),
    (II, 1, (1,), 1, (1,), ()): ("1", "1"),
    (MM, 1, (1,), 1, (1,), ()): ("1", "1"),
    (II, 1, (1,), 2, (1,), ()): ("1", "1;R"),
    (MM, 1, (1,), 2, (1,), ()): ("1", "1;R"),
    (II, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
    (MM, 0, (1,), 1, (2,), ()): ("L", "L;2I"),
    (II, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
    (MM, 0, (1,), 2, (2,), ()): ("L", "L;2IR"),
    (II, 1, (1,), 1, (2,), ()): ("L", "L;2"),
    (MM, 1, (1,), 1, (2,), ()): ("L", "L;2"),
    (II, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
    (MM, 1, (1,), 2, (2,), ()): ("L", "L;2R"),
    (II, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
    (MM, 0, (1,), 1, (4,), ()): ("L", "L;4I"),
    (II, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
    (MM, 0, (1,), 2, (4,), ()): ("L", "L;4IR"),
    (II, 1, (1,), 1, (4,), ()): ("L", "L;4"),
    (MM, 1, (1,), 1, (4,), ()): ("L", "L;4"),
    (II, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
    (MM, 1, (1,), 2, (4,), ()): ("L", "L;4R"),
    (II, 0, (1,), 1, (8,), ()): ("L", "L;I"),
    (MM, 0, (1,), 1, (8,), ()): ("L", "L;I"),
    (II, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
    (MM, 0, (1,), 2, (8,), ()): ("L", "L;IR"),
    (II, 1, (1,), 1, (8,), ()): ("L", "L"),
    (MM, 1, (1,), 1, (8,), ()): ("L", "L"),
    (II, 1, (1,), 2, (8,), ()): ("L", "L;R"),
    (MM, 1, (1,), 2, (8,), ()): ("L", "L;R"),
    (II, 1, (1,), 1, (12,), ()): ("I;16", "I;12"),
    (II, 0, (1,), 1, (16,), ()): ("I;16", "I;16"),
    (II, 1, (1,), 1, (16,), ()): ("I;16", "I;16"),
    (MM, 1, (1,), 1, (16,), ()): ("I;16B", "I;16B"),
    (II, 1, (1,), 2, (16,), ()): ("I;16", "I;16R"),
    (II, 1, (2,), 1, (16,), ()): ("I", "I;16S"),
    (MM, 1, (2,), 1, (16,), ()): ("I", "I;16BS"),
    (II, 0, (3,), 1, (32,), ()): ("F", "F;32F"),
    (MM, 0, (3,), 1, (32,), ()): ("F", "F;32BF"),
    (II, 1, (1,), 1, (32,), ()): ("I", "I;32N"),
    (II, 1, (2,), 1, (32,), ()): ("I", "I;32S"),
    (MM, 1, (2,), 1, (32,), ()): ("I", "I;32BS"),
    (II, 1, (3,), 1, (32,), ()): ("F", "F;32F"),
    (MM, 1, (3,), 1, (32,), ()): ("F", "F;32BF"),
    (II, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
    (MM, 1, (1,), 1, (8, 8), (2,)): ("LA", "LA"),
    (II, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
    (MM, 2, (1,), 1, (8, 8, 8), ()): ("RGB", "RGB"),
    (II, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
    (MM, 2, (1,), 2, (8, 8, 8), ()): ("RGB", "RGB;R"),
    (II, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"),  # missing ExtraSamples
    (MM, 2, (1,), 1, (8, 8, 8, 8), ()): ("RGBA", "RGBA"),  # missing ExtraSamples
    (II, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8), (0,)): ("RGBX", "RGBX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (0, 0)): ("RGBX", "RGBXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0, 0)): ("RGBX", "RGBXXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
    (MM, 2, (1,), 1, (8, 8, 8, 8), (1,)): ("RGBA", "RGBa"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (1, 0)): ("RGBA", "RGBaX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (1, 0, 0)): ("RGBA", "RGBaXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
    (MM, 2, (1,), 1, (8, 8, 8, 8), (2,)): ("RGBA", "RGBA"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8), (2, 0)): ("RGBA", "RGBAX"),
    (II, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
    (MM, 2, (1,), 1, (8, 8, 8, 8, 8, 8), (2, 0, 0)): ("RGBA", "RGBAXX"),
    (II, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"),  # Corel Draw 10
    (MM, 2, (1,), 1, (8, 8, 8, 8), (999,)): ("RGBA", "RGBA"),  # Corel Draw 10
    (II, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16L"),
    (MM, 2, (1,), 1, (16, 16, 16), ()): ("RGB", "RGB;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), ()): ("RGBA", "RGBA;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), (0,)): ("RGBX", "RGBX;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), (1,)): ("RGBA", "RGBa;16B"),
    (II, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16L"),
    (MM, 2, (1,), 1, (16, 16, 16, 16), (2,)): ("RGBA", "RGBA;16B"),
    (II, 3, (1,), 1, (1,), ()): ("P", "P;1"),
    (MM, 3, (1,), 1, (1,), ()): ("P", "P;1"),
    (II, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
    (MM, 3, (1,), 2, (1,), ()): ("P", "P;1R"),
    (II, 3, (1,), 1, (2,), ()): ("P", "P;2"),
    (MM, 3, (1,), 1, (2,), ()): ("P", "P;2"),
    (II, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
    (MM, 3, (1,), 2, (2,), ()): ("P", "P;2R"),
    (II, 3, (1,), 1, (4,), ()): ("P", "P;4"),
    (MM, 3, (1,), 1, (4,), ()): ("P", "P;4"),
    (II, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
    (MM, 3, (1,), 2, (4,), ()): ("P", "P;4R"),
    (II, 3, (1,), 1, (8,), ()): ("P", "P"),
    (MM, 3, (1,), 1, (8,), ()): ("P", "P"),
    (II, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
    (MM, 3, (1,), 1, (8, 8), (2,)): ("PA", "PA"),
    (II, 3, (1,), 2, (8,), ()): ("P", "P;R"),
    (MM, 3, (1,), 2, (8,), ()): ("P", "P;R"),
    (II, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
    (MM, 5, (1,), 1, (8, 8, 8, 8), ()): ("CMYK", "CMYK"),
    (II, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
    (MM, 5, (1,), 1, (8, 8, 8, 8, 8), (0,)): ("CMYK", "CMYKX"),
    (II, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
    (MM, 5, (1,), 1, (8, 8, 8, 8, 8, 8), (0, 0)): ("CMYK", "CMYKXX"),
    (II, 5, (1,), 1, (16, 16, 16, 16), ()): ("CMYK", "CMYK;16L"),
    # JPEG compressed images handled by LibTiff and auto-converted to RGBX
    # Minimal Baseline TIFF requires YCbCr images to have 3 SamplesPerPixel
    (II, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
    (MM, 6, (1,), 1, (8, 8, 8), ()): ("RGB", "RGBX"),
    (II, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
    (MM, 8, (1,), 1, (8, 8, 8), ()): ("LAB", "LAB"),
}

MAX_SAMPLESPERPIXEL = max(len(key_tp[4]) for key_tp in OPEN_INFO.keys())

PREFIXES = [
    b"MM\x00\x2A",  # Valid TIFF header with big-endian byte order
    b"II\x2A\x00",  # Valid TIFF header with little-endian byte order
    b"MM\x2A\x00",  # Invalid TIFF header, assume big-endian
    b"II\x00\x2A",  # Invalid TIFF header, assume little-endian
    b"MM\x00\x2B",  # BigTIFF with big-endian byte order
    b"II\x2B\x00",  # BigTIFF with little-endian byte order
]


def _accept(prefix):
    return prefix[:4] in PREFIXES


def _limit_rational(val, max_val):
    inv = abs(val) > 1
    n_d = IFDRational(1 / val if inv else val).limit_rational(max_val)
    return n_d[::-1] if inv else n_d


def _limit_signed_rational(val, max_val, min_val):
    frac = Fraction(val)
    n_d = frac.numerator, frac.denominator

    if min(n_d) < min_val:
        n_d = _limit_rational(val, abs(min_val))

    if max(n_d) > max_val:
        val = Fraction(*n_d)
        n_d = _limit_rational(val, max_val)

    return n_d


##
# Wrapper for TIFF IFDs.

_load_dispatch = {}
_write_dispatch = {}


class IFDRational(Rational):
    """Implements a rational class where 0/0 is a legal value to match
    the in the wild use of exif rationals.

    e.g., DigitalZoomRatio - 0.00/0.00  indicates that no digital zoom was used
    """

    """ If the denominator is 0, store this as a float('nan'), otherwise store
    as a fractions.Fraction(). Delegate as appropriate

    """

    __slots__ = ("_numerator", "_denominator", "_val")

    def __init__(self, value, denominator=1):
        """
        :param value: either an integer numerator, a
        float/rational/other number, or an IFDRational
        :param denominator: Optional integer denominator
        """
        if isinstance(value, IFDRational):
            self._numerator = value.numerator
            self._denominator = value.denominator
            self._val = value._val
            return

        if isinstance(value, Fraction):
            self._numerator = value.numerator
            self._denominator = value.denominator
        else:
            self._numerator = value
            self._denominator = denominator

        if denominator == 0:
            self._val = float("nan")
        elif denominator == 1:
            self._val = Fraction(value)
        else:
            self._val = Fraction(value, denominator)

    @property
    def numerator(self):
        return self._numerator

    @property
    def denominator(self):
        return self._denominator

    def limit_rational(self, max_denominator):
        """

        :param max_denominator: Integer, the maximum denominator value
        :returns: Tuple of (numerator, denominator)
        """

        if self.denominator == 0:
            return self.numerator, self.denominator

        f = self._val.limit_denominator(max_denominator)
        return f.numerator, f.denominator

    def __repr__(self):
        return str(float(self._val))

    def __hash__(self):
        return self._val.__hash__()

    def __eq__(self, other):
        val = self._val
        if isinstance(other, IFDRational):
            other = other._val
        if isinstance(other, float):
            val = float(val)
        return val == other

    def __getstate__(self):
        return [self._val, self._numerator, self._denominator]

    def __setstate__(self, state):
        IFDRational.__init__(self, 0)
        _val, _numerator, _denominator = state
        self._val = _val
        self._numerator = _numerator
        self._denominator = _denominator

    def _delegate(op):
        def delegate(self, *args):
            return getattr(self._val, op)(*args)

        return delegate

    """ a = ['add','radd', 'sub', 'rsub', 'mul', 'rmul',
             'truediv', 'rtruediv', 'floordiv', 'rfloordiv',
             'mod','rmod', 'pow','rpow', 'pos', 'neg',
             'abs', 'trunc', 'lt', 'gt', 'le', 'ge', 'bool',
             'ceil', 'floor', 'round']
        print("\n".join("__%s__ = _delegate('__%s__')" % (s,s) for s in a))
        """

    __add__ = _delegate("__add__")
    __radd__ = _delegate("__radd__")
    __sub__ = _delegate("__sub__")
    __rsub__ = _delegate("__rsub__")
    __mul__ = _delegate("__mul__")
    __rmul__ = _delegate("__rmul__")
    __truediv__ = _delegate("__truediv__")
    __rtruediv__ = _delegate("__rtruediv__")
    __floordiv__ = _delegate("__floordiv__")
    __rfloordiv__ = _delegate("__rfloordiv__")
    __mod__ = _delegate("__mod__")
    __rmod__ = _delegate("__rmod__")
    __pow__ = _delegate("__pow__")
    __rpow__ = _delegate("__rpow__")
    __pos__ = _delegate("__pos__")
    __neg__ = _delegate("__neg__")
    __abs__ = _delegate("__abs__")
    __trunc__ = _delegate("__trunc__")
    __lt__ = _delegate("__lt__")
    __gt__ = _delegate("__gt__")
    __le__ = _delegate("__le__")
    __ge__ = _delegate("__ge__")
    __bool__ = _delegate("__bool__")
    __ceil__ = _delegate("__ceil__")
    __floor__ = _delegate("__floor__")
    __round__ = _delegate("__round__")


class ImageFileDirectory_v2(MutableMapping):
    """This class represents a TIFF tag directory.  To speed things up, we
    don't decode tags unless they're asked for.

    Exposes a dictionary interface of the tags in the directory::

        ifd = ImageFileDirectory_v2()
        ifd[key] = 'Some Data'
        ifd.tagtype[key] = TiffTags.ASCII
        print(ifd[key])
        'Some Data'

    Individual values are returned as the strings or numbers, sequences are
    returned as tuples of the values.

    The tiff metadata type of each item is stored in a dictionary of
    tag types in
    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v2.tagtype`. The types
    are read from a tiff file, guessed from the type added, or added
    manually.

    Data Structures:

        * ``self.tagtype = {}``

          * Key: numerical TIFF tag number
          * Value: integer corresponding to the data type from
            :py:data:`.TiffTags.TYPES`

          .. versionadded:: 3.0.0

    'Internal' data structures:

        * ``self._tags_v2 = {}``

          * Key: numerical TIFF tag number
          * Value: decoded data, as tuple for multiple values

        * ``self._tagdata = {}``

          * Key: numerical TIFF tag number
          * Value: undecoded byte string from file

        * ``self._tags_v1 = {}``

          * Key: numerical TIFF tag number
          * Value: decoded data in the v1 format

    Tags will be found in the private attributes ``self._tagdata``, and in
    ``self._tags_v2`` once decoded.

    ``self.legacy_api`` is a value for internal use, and shouldn't be changed
    from outside code. In cooperation with
    :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`, if ``legacy_api``
    is true, then decoded tags will be populated into both ``_tags_v1`` and
    ``_tags_v2``. ``_tags_v2`` will be used if this IFD is used in the TIFF
    save routine. Tags should be read from ``_tags_v1`` if
    ``legacy_api == true``.

    """

    def __init__(self, ifh=b"II\052\0\0\0\0\0", prefix=None, group=None):
        """Initialize an ImageFileDirectory.

        To construct an ImageFileDirectory from a real file, pass the 8-byte
        magic header to the constructor.  To only set the endianness, pass it
        as the 'prefix' keyword argument.

        :param ifh: One of the accepted magic headers (cf. PREFIXES); also sets
              endianness.
        :param prefix: Override the endianness of the file.
        """
        if not _accept(ifh):
            raise SyntaxError(f"not a TIFF file (header {repr(ifh)} not valid)")
        self._prefix = prefix if prefix is not None else ifh[:2]
        if self._prefix == MM:
            self._endian = ">"
        elif self._prefix == II:
            self._endian = "<"
        else:
            raise SyntaxError("not a TIFF IFD")
        self._bigtiff = ifh[2] == 43
        self.group = group
        self.tagtype = {}
        """ Dictionary of tag types """
        self.reset()
        (self.next,) = (
            self._unpack("Q", ifh[8:]) if self._bigtiff else self._unpack("L", ifh[4:])
        )
        self._legacy_api = False

    prefix = property(lambda self: self._prefix)
    offset = property(lambda self: self._offset)
    legacy_api = property(lambda self: self._legacy_api)

    @legacy_api.setter
    def legacy_api(self, value):
        raise Exception("Not allowing setting of legacy api")

    def reset(self):
        self._tags_v1 = {}  # will remain empty if legacy_api is false
        self._tags_v2 = {}  # main tag storage
        self._tagdata = {}
        self.tagtype = {}  # added 2008-06-05 by Florian Hoech
        self._next = None
        self._offset = None

    def __str__(self):
        return str(dict(self))

    def named(self):
        """
        :returns: dict of name|key: value

        Returns the complete tag dictionary, with named tags where possible.
        """
        return {
            TiffTags.lookup(code, self.group).name: value
            for code, value in self.items()
        }

    def __len__(self):
        return len(set(self._tagdata) | set(self._tags_v2))

    def __getitem__(self, tag):
        if tag not in self._tags_v2:  # unpack on the fly
            data = self._tagdata[tag]
            typ = self.tagtype[tag]
            size, handler = self._load_dispatch[typ]
            self[tag] = handler(self, data, self.legacy_api)  # check type
        val = self._tags_v2[tag]
        if self.legacy_api and not isinstance(val, (tuple, bytes)):
            val = (val,)
        return val

    def __contains__(self, tag):
        return tag in self._tags_v2 or tag in self._tagdata

    def __setitem__(self, tag, value):
        self._setitem(tag, value, self.legacy_api)

    def _setitem(self, tag, value, legacy_api):
        basetypes = (Number, bytes, str)

        info = TiffTags.lookup(tag, self.group)
        values = [value] if isinstance(value, basetypes) else value

        if tag not in self.tagtype:
            if info.type:
                self.tagtype[tag] = info.type
            else:
                self.tagtype[tag] = TiffTags.UNDEFINED
                if all(isinstance(v, IFDRational) for v in values):
                    self.tagtype[tag] = (
                        TiffTags.RATIONAL
                        if all(v >= 0 for v in values)
                        else TiffTags.SIGNED_RATIONAL
                    )
                elif all(isinstance(v, int) for v in values):
                    if all(0 <= v < 2**16 for v in values):
                        self.tagtype[tag] = TiffTags.SHORT
                    elif all(-(2**15) < v < 2**15 for v in values):
                        self.tagtype[tag] = TiffTags.SIGNED_SHORT
                    else:
                        self.tagtype[tag] = (
                            TiffTags.LONG
                            if all(v >= 0 for v in values)
                            else TiffTags.SIGNED_LONG
                        )
                elif all(isinstance(v, float) for v in values):
                    self.tagtype[tag] = TiffTags.DOUBLE
                elif all(isinstance(v, str) for v in values):
                    self.tagtype[tag] = TiffTags.ASCII
                elif all(isinstance(v, bytes) for v in values):
                    self.tagtype[tag] = TiffTags.BYTE

        if self.tagtype[tag] == TiffTags.UNDEFINED:
            values = [
                v.encode("ascii", "replace") if isinstance(v, str) else v
                for v in values
            ]
        elif self.tagtype[tag] == TiffTags.RATIONAL:
            values = [float(v) if isinstance(v, int) else v for v in values]

        is_ifd = self.tagtype[tag] == TiffTags.LONG and isinstance(values, dict)
        if not is_ifd:
            values = tuple(info.cvt_enum(value) for value in values)

        dest = self._tags_v1 if legacy_api else self._tags_v2

        # Three branches:
        # Spec'd length == 1, Actual length 1, store as element
        # Spec'd length == 1, Actual > 1, Warn and truncate. Formerly barfed.
        # No Spec, Actual length 1, Formerly (<4.2) returned a 1 element tuple.
        # Don't mess with the legacy api, since it's frozen.
        if not is_ifd and (
            (info.length == 1)
            or self.tagtype[tag] == TiffTags.BYTE
            or (info.length is None and len(values) == 1 and not legacy_api)
        ):
            # Don't mess with the legacy api, since it's frozen.
            if legacy_api and self.tagtype[tag] in [
                TiffTags.RATIONAL,
                TiffTags.SIGNED_RATIONAL,
            ]:  # rationals
                values = (values,)
            try:
                (dest[tag],) = values
            except ValueError:
                # We've got a builtin tag with 1 expected entry
                warnings.warn(
                    f"Metadata Warning, tag {tag} had too many entries: "
                    f"{len(values)}, expected 1"
                )
                dest[tag] = values[0]

        else:
            # Spec'd length > 1 or undefined
            # Unspec'd, and length > 1
            dest[tag] = values

    def __delitem__(self, tag):
        self._tags_v2.pop(tag, None)
        self._tags_v1.pop(tag, None)
        self._tagdata.pop(tag, None)

    def __iter__(self):
        return iter(set(self._tagdata) | set(self._tags_v2))

    def _unpack(self, fmt, data):
        return struct.unpack(self._endian + fmt, data)

    def _pack(self, fmt, *values):
        return struct.pack(self._endian + fmt, *values)

    def _register_loader(idx, size):
        def decorator(func):
            from .TiffTags import TYPES

            if func.__name__.startswith("load_"):
                TYPES[idx] = func.__name__[5:].replace("_", " ")
            _load_dispatch[idx] = size, func  # noqa: F821
            return func

        return decorator

    def _register_writer(idx):
        def decorator(func):
            _write_dispatch[idx] = func  # noqa: F821
            return func

        return decorator

    def _register_basic(idx_fmt_name):
        from .TiffTags import TYPES

        idx, fmt, name = idx_fmt_name
        TYPES[idx] = name
        size = struct.calcsize("=" + fmt)
        _load_dispatch[idx] = (  # noqa: F821
            size,
            lambda self, data, legacy_api=True: (
                self._unpack(f"{len(data) // size}{fmt}", data)
            ),
        )
        _write_dispatch[idx] = lambda self, *values: (  # noqa: F821
            b"".join(self._pack(fmt, value) for value in values)
        )

    list(
        map(
            _register_basic,
            [
                (TiffTags.SHORT, "H", "short"),
                (TiffTags.LONG, "L", "long"),
                (TiffTags.SIGNED_BYTE, "b", "signed byte"),
                (TiffTags.SIGNED_SHORT, "h", "signed short"),
                (TiffTags.SIGNED_LONG, "l", "signed long"),
                (TiffTags.FLOAT, "f", "float"),
                (TiffTags.DOUBLE, "d", "double"),
                (TiffTags.IFD, "L", "long"),
                (TiffTags.LONG8, "Q", "long8"),
            ],
        )
    )

    @_register_loader(1, 1)  # Basic type, except for the legacy API.
    def load_byte(self, data, legacy_api=True):
        return data

    @_register_writer(1)  # Basic type, except for the legacy API.
    def write_byte(self, data):
        if isinstance(data, int):
            data = bytes((data,))
        return data

    @_register_loader(2, 1)
    def load_string(self, data, legacy_api=True):
        if data.endswith(b"\0"):
            data = data[:-1]
        return data.decode("latin-1", "replace")

    @_register_writer(2)
    def write_string(self, value):
        # remerge of https://github.com/python-pillow/Pillow/pull/1416
        if not isinstance(value, bytes):
            value = value.encode("ascii", "replace")
        return value + b"\0"

    @_register_loader(5, 8)
    def load_rational(self, data, legacy_api=True):
        vals = self._unpack(f"{len(data) // 4}L", data)

        def combine(a, b):
            return (a, b) if legacy_api else IFDRational(a, b)

        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))

    @_register_writer(5)
    def write_rational(self, *values):
        return b"".join(
            self._pack("2L", *_limit_rational(frac, 2**32 - 1)) for frac in values
        )

    @_register_loader(7, 1)
    def load_undefined(self, data, legacy_api=True):
        return data

    @_register_writer(7)
    def write_undefined(self, value):
        return value

    @_register_loader(10, 8)
    def load_signed_rational(self, data, legacy_api=True):
        vals = self._unpack(f"{len(data) // 4}l", data)

        def combine(a, b):
            return (a, b) if legacy_api else IFDRational(a, b)

        return tuple(combine(num, denom) for num, denom in zip(vals[::2], vals[1::2]))

    @_register_writer(10)
    def write_signed_rational(self, *values):
        return b"".join(
            self._pack("2l", *_limit_signed_rational(frac, 2**31 - 1, -(2**31)))
            for frac in values
        )

    def _ensure_read(self, fp, size):
        ret = fp.read(size)
        if len(ret) != size:
            raise OSError(
                "Corrupt EXIF data.  "
                f"Expecting to read {size} bytes but only got {len(ret)}. "
            )
        return ret

    def load(self, fp):

        self.reset()
        self._offset = fp.tell()

        try:
            tag_count = (
                self._unpack("Q", self._ensure_read(fp, 8))
                if self._bigtiff
                else self._unpack("H", self._ensure_read(fp, 2))
            )[0]
            for i in range(tag_count):
                tag, typ, count, data = (
                    self._unpack("HHQ8s", self._ensure_read(fp, 20))
                    if self._bigtiff
                    else self._unpack("HHL4s", self._ensure_read(fp, 12))
                )

                tagname = TiffTags.lookup(tag, self.group).name
                typname = TYPES.get(typ, "unknown")
                msg = f"tag: {tagname} ({tag}) - type: {typname} ({typ})"

                try:
                    unit_size, handler = self._load_dispatch[typ]
                except KeyError:
                    logger.debug(msg + f" - unsupported type {typ}")
                    continue  # ignore unsupported type
                size = count * unit_size
                if size > (8 if self._bigtiff else 4):
                    here = fp.tell()
                    (offset,) = self._unpack("Q" if self._bigtiff else "L", data)
                    msg += f" Tag Location: {here} - Data Location: {offset}"
                    fp.seek(offset)
                    data = ImageFile._safe_read(fp, size)
                    fp.seek(here)
                else:
                    data = data[:size]

                if len(data) != size:
                    warnings.warn(
                        "Possibly corrupt EXIF data.  "
                        f"Expecting to read {size} bytes but only got {len(data)}."
                        f" Skipping tag {tag}"
                    )
                    logger.debug(msg)
                    continue

                if not data:
                    logger.debug(msg)
                    continue

                self._tagdata[tag] = data
                self.tagtype[tag] = typ

                msg += " - value: " + (
                    "<table: %d bytes>" % size if size > 32 else repr(data)
                )
                logger.debug(msg)

            (self.next,) = (
                self._unpack("Q", self._ensure_read(fp, 8))
                if self._bigtiff
                else self._unpack("L", self._ensure_read(fp, 4))
            )
        except OSError as msg:
            warnings.warn(str(msg))
            return

    def tobytes(self, offset=0):
        # FIXME What about tagdata?
        result = self._pack("H", len(self._tags_v2))

        entries = []
        offset = offset + len(result) + len(self._tags_v2) * 12 + 4
        stripoffsets = None

        # pass 1: convert tags to binary format
        # always write tags in ascending order
        for tag, value in sorted(self._tags_v2.items()):
            if tag == STRIPOFFSETS:
                stripoffsets = len(entries)
            typ = self.tagtype.get(tag)
            logger.debug(f"Tag {tag}, Type: {typ}, Value: {repr(value)}")
            is_ifd = typ == TiffTags.LONG and isinstance(value, dict)
            if is_ifd:
                if self._endian == "<":
                    ifh = b"II\x2A\x00\x08\x00\x00\x00"
                else:
                    ifh = b"MM\x00\x2A\x00\x00\x00\x08"
                ifd = ImageFileDirectory_v2(ifh, group=tag)
                values = self._tags_v2[tag]
                for ifd_tag, ifd_value in values.items():
                    ifd[ifd_tag] = ifd_value
                data = ifd.tobytes(offset)
            else:
                values = value if isinstance(value, tuple) else (value,)
                data = self._write_dispatch[typ](self, *values)

            tagname = TiffTags.lookup(tag, self.group).name
            typname = "ifd" if is_ifd else TYPES.get(typ, "unknown")
            msg = f"save: {tagname} ({tag}) - type: {typname} ({typ})"
            msg += " - value: " + (
                "<table: %d bytes>" % len(data) if len(data) >= 16 else str(values)
            )
            logger.debug(msg)

            # count is sum of lengths for string and arbitrary data
            if is_ifd:
                count = 1
            elif typ in [TiffTags.BYTE, TiffTags.ASCII, TiffTags.UNDEFINED]:
                count = len(data)
            else:
                count = len(values)
            # figure out if data fits into the entry
            if len(data) <= 4:
                entries.append((tag, typ, count, data.ljust(4, b"\0"), b""))
            else:
                entries.append((tag, typ, count, self._pack("L", offset), data))
                offset += (len(data) + 1) // 2 * 2  # pad to word

        # update strip offset data to point beyond auxiliary data
        if stripoffsets is not None:
            tag, typ, count, value, data = entries[stripoffsets]
            if data:
                raise NotImplementedError("multistrip support not yet implemented")
            value = self._pack("L", self._unpack("L", value)[0] + offset)
            entries[stripoffsets] = tag, typ, count, value, data

        # pass 2: write entries to file
        for tag, typ, count, value, data in entries:
            logger.debug(f"{tag} {typ} {count} {repr(value)} {repr(data)}")
            result += self._pack("HHL4s", tag, typ, count, value)

        # -- overwrite here for multi-page --
        result += b"\0\0\0\0"  # end of entries

        # pass 3: write auxiliary data to file
        for tag, typ, count, value, data in entries:
            result += data
            if len(data) & 1:
                result += b"\0"

        return result

    def save(self, fp):

        if fp.tell() == 0:  # skip TIFF header on subsequent pages
            # tiff header -- PIL always starts the first IFD at offset 8
            fp.write(self._prefix + self._pack("HL", 42, 8))

        offset = fp.tell()
        result = self.tobytes(offset)
        fp.write(result)
        return offset + len(result)


ImageFileDirectory_v2._load_dispatch = _load_dispatch
ImageFileDirectory_v2._write_dispatch = _write_dispatch
for idx, name in TYPES.items():
    name = name.replace(" ", "_")
    setattr(ImageFileDirectory_v2, "load_" + name, _load_dispatch[idx][1])
    setattr(ImageFileDirectory_v2, "write_" + name, _write_dispatch[idx])
del _load_dispatch, _write_dispatch, idx, name


# Legacy ImageFileDirectory support.
class ImageFileDirectory_v1(ImageFileDirectory_v2):
    """This class represents the **legacy** interface to a TIFF tag directory.

    Exposes a dictionary interface of the tags in the directory::

        ifd = ImageFileDirectory_v1()
        ifd[key] = 'Some Data'
        ifd.tagtype[key] = TiffTags.ASCII
        print(ifd[key])
        ('Some Data',)

    Also contains a dictionary of tag types as read from the tiff image file,
    :attr:`~PIL.TiffImagePlugin.ImageFileDirectory_v1.tagtype`.

    Values are returned as a tuple.

    ..  deprecated:: 3.0.0
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._legacy_api = True

    tags = property(lambda self: self._tags_v1)
    tagdata = property(lambda self: self._tagdata)

    # defined in ImageFileDirectory_v2
    tagtype: dict
    """Dictionary of tag types"""

    @classmethod
    def from_v2(cls, original):
        """Returns an
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
        instance with the same data as is contained in the original
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
        instance.

        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`

        """

        ifd = cls(prefix=original.prefix)
        ifd._tagdata = original._tagdata
        ifd.tagtype = original.tagtype
        ifd.next = original.next  # an indicator for multipage tiffs
        return ifd

    def to_v2(self):
        """Returns an
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`
        instance with the same data as is contained in the original
        :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v1`
        instance.

        :returns: :py:class:`~PIL.TiffImagePlugin.ImageFileDirectory_v2`

        """

        ifd = ImageFileDirectory_v2(prefix=self.prefix)
        ifd._tagdata = dict(self._tagdata)
        ifd.tagtype = dict(self.tagtype)
        ifd._tags_v2 = dict(self._tags_v2)
        return ifd

    def __contains__(self, tag):
        return tag in self._tags_v1 or tag in self._tagdata

    def __len__(self):
        return len(set(self._tagdata) | set(self._tags_v1))

    def __iter__(self):
        return iter(set(self._tagdata) | set(self._tags_v1))

    def __setitem__(self, tag, value):
        for legacy_api in (False, True):
            self._setitem(tag, value, legacy_api)

    def __getitem__(self, tag):
        if tag not in self._tags_v1:  # unpack on the fly
            data = self._tagdata[tag]
            typ = self.tagtype[tag]
            size, handler = self._load_dispatch[typ]
            for legacy in (False, True):
                self._setitem(tag, handler(self, data, legacy), legacy)
        val = self._tags_v1[tag]
        if not isinstance(val, (tuple, bytes)):
            val = (val,)
        return val


# undone -- switch this pointer when IFD_LEGACY_API == False
ImageFileDirectory = ImageFileDirectory_v1


##
# Image plugin for TIFF files.


class TiffImageFile(ImageFile.ImageFile):

    format = "TIFF"
    format_description = "Adobe TIFF"
    _close_exclusive_fp_after_loading = False

    def __init__(self, fp=None, filename=None):
        self.tag_v2 = None
        """ Image file directory (tag dictionary) """

        self.tag = None
        """ Legacy tag entries """

        super().__init__(fp, filename)

    def _open(self):
        """Open the first image in a TIFF file"""

        # Header
        ifh = self.fp.read(8)
        if ifh[2] == 43:
            ifh += self.fp.read(8)

        self.tag_v2 = ImageFileDirectory_v2(ifh)

        # legacy IFD entries will be filled in later
        self.ifd = None

        # setup frame pointers
        self.__first = self.__next = self.tag_v2.next
        self.__frame = -1
        self._fp = self.fp
        self._frame_pos = []
        self._n_frames = None

        logger.debug("*** TiffImageFile._open ***")
        logger.debug(f"- __first: {self.__first}")
        logger.debug(f"- ifh: {repr(ifh)}")  # Use repr to avoid str(bytes)

        # and load the first frame
        self._seek(0)

    @property
    def n_frames(self):
        if self._n_frames is None:
            current = self.tell()
            self._seek(len(self._frame_pos))
            while self._n_frames is None:
                self._seek(self.tell() + 1)
            self.seek(current)
        return self._n_frames

    def seek(self, frame):
        """Select a given frame as current image"""
        if not self._seek_check(frame):
            return
        self._seek(frame)
        # Create a new core image object on second and
        # subsequent frames in the image. Image may be
        # different size/mode.
        Image._decompression_bomb_check(self.size)
        self.im = Image.core.new(self.mode, self.size)

    def _seek(self, frame):
        self.fp = self._fp

        # reset buffered io handle in case fp
        # was passed to libtiff, invalidating the buffer
        self.fp.tell()

        while len(self._frame_pos) <= frame:
            if not self.__next:
                raise EOFError("no more images in TIFF file")
            logger.debug(
                f"Seeking to frame {frame}, on frame {self.__frame}, "
                f"__next {self.__next}, location: {self.fp.tell()}"
            )
            self.fp.seek(self.__next)
            self._frame_pos.append(self.__next)
            logger.debug("Loading tags, location: %s" % self.fp.tell())
            self.tag_v2.load(self.fp)
            if self.tag_v2.next in self._frame_pos:
                # This IFD has already been processed
                # Declare this to be the end of the image
                self.__next = 0
            else:
                self.__next = self.tag_v2.next
            if self.__next == 0:
                self._n_frames = frame + 1
            if len(self._frame_pos) == 1:
                self.is_animated = self.__next != 0
            self.__frame += 1
        self.fp.seek(self._frame_pos[frame])
        self.tag_v2.load(self.fp)
        self._reload_exif()
        # fill the legacy tag/ifd entries
        self.tag = self.ifd = ImageFileDirectory_v1.from_v2(self.tag_v2)
        self.__frame = frame
        self._setup()

    def tell(self):
        """Return the current frame number"""
        return self.__frame

    def get_child_images(self):
        if SUBIFD not in self.tag_v2:
            return []
        child_images = []
        exif = self.getexif()
        offset = None
        for im_offset in self.tag_v2[SUBIFD]:
            # reset buffered io handle in case fp
            # was passed to libtiff, invalidating the buffer
            current_offset = self._fp.tell()
            if offset is None:
                offset = current_offset

            fp = self._fp
            ifd = exif._get_ifd_dict(im_offset)
            jpegInterchangeFormat = ifd.get(513)
            if jpegInterchangeFormat is not None:
                fp.seek(jpegInterchangeFormat)
                jpeg_data = fp.read(ifd.get(514))

                fp = io.BytesIO(jpeg_data)

            with Image.open(fp) as im:
                if jpegInterchangeFormat is None:
                    im._frame_pos = [im_offset]
                    im._seek(0)
                im.load()
                child_images.append(im)

        if offset is not None:
            self._fp.seek(offset)
        return child_images

    def getxmp(self):
        """
        Returns a dictionary containing the XMP tags.
        Requires defusedxml to be installed.

        :returns: XMP tags in a dictionary.
        """
        return self._getxmp(self.tag_v2[XMP]) if XMP in self.tag_v2 else {}

    def get_photoshop_blocks(self):
        """
        Returns a dictionary of Photoshop "Image Resource Blocks".
        The keys are the image resource ID. For more information, see
        https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_pgfId-1037727

        :returns: Photoshop "Image Resource Blocks" in a dictionary.
        """
        blocks = {}
        val = self.tag_v2.get(0x8649)
        if val:
            while val[:4] == b"8BIM":
                id = i16(val[4:6])
                n = math.ceil((val[6] + 1) / 2) * 2
                size = i32(val[6 + n : 10 + n])
                data = val[10 + n : 10 + n + size]
                blocks[id] = {"data": data}

                val = val[math.ceil((10 + n + size) / 2) * 2 :]
        return blocks

    def load(self):
        if self.tile and self.use_load_libtiff:
            return self._load_libtiff()
        return super().load()

    def load_end(self):
        if self._tile_orientation:
            method = {
                2: Image.Transpose.FLIP_LEFT_RIGHT,
                3: Image.Transpose.ROTATE_180,
                4: Image.Transpose.FLIP_TOP_BOTTOM,
                5: Image.Transpose.TRANSPOSE,
                6: Image.Transpose.ROTATE_270,
                7: Image.Transpose.TRANSVERSE,
                8: Image.Transpose.ROTATE_90,
            }.get(self._tile_orientation)
            if method is not None:
                self.im = self.im.transpose(method)
                self._size = self.im.size

        # allow closing if we're on the first frame, there's no next
        # This is the ImageFile.load path only, libtiff specific below.
        if not self.is_animated:
            self._close_exclusive_fp_after_loading = True

            # reset buffered io handle in case fp
            # was passed to libtiff, invalidating the buffer
            self.fp.tell()

            # load IFD data from fp before it is closed
            exif = self.getexif()
            for key in TiffTags.TAGS_V2_GROUPS.keys():
                if key not in exif:
                    continue
                exif.get_ifd(key)

    def _load_libtiff(self):
        """Overload method triggered when we detect a compressed tiff
        Calls out to libtiff"""

        Image.Image.load(self)

        self.load_prepare()

        if not len(self.tile) == 1:
            raise OSError("Not exactly one tile")

        # (self._compression, (extents tuple),
        #   0, (rawmode, self._compression, fp))
        extents = self.tile[0][1]
        args = list(self.tile[0][3])

        # To be nice on memory footprint, if there's a
        # file descriptor, use that instead of reading
        # into a string in python.
        # libtiff closes the file descriptor, so pass in a dup.
        try:
            fp = hasattr(self.fp, "fileno") and os.dup(self.fp.fileno())
            # flush the file descriptor, prevents error on pypy 2.4+
            # should also eliminate the need for fp.tell
            # in _seek
            if hasattr(self.fp, "flush"):
                self.fp.flush()
        except OSError:
            # io.BytesIO have a fileno, but returns an OSError if
            # it doesn't use a file descriptor.
            fp = False

        if fp:
            args[2] = fp

        decoder = Image._getdecoder(
            self.mode, "libtiff", tuple(args), self.decoderconfig
        )
        try:
            decoder.setimage(self.im, extents)
        except ValueError as e:
            raise OSError("Couldn't set the image") from e

        close_self_fp = self._exclusive_fp and not self.is_animated
        if hasattr(self.fp, "getvalue"):
            # We've got a stringio like thing passed in. Yay for all in memory.
            # The decoder needs the entire file in one shot, so there's not
            # a lot we can do here other than give it the entire file.
            # unless we could do something like get the address of the
            # underlying string for stringio.
            #
            # Rearranging for supporting byteio items, since they have a fileno
            # that returns an OSError if there's no underlying fp. Easier to
            # deal with here by reordering.
            logger.debug("have getvalue. just sending in a string from getvalue")
            n, err = decoder.decode(self.fp.getvalue())
        elif fp:
            # we've got a actual file on disk, pass in the fp.
            logger.debug("have fileno, calling fileno version of the decoder.")
            if not close_self_fp:
                self.fp.seek(0)
            # 4 bytes, otherwise the trace might error out
            n, err = decoder.decode(b"fpfp")
        else:
            # we have something else.
            logger.debug("don't have fileno or getvalue. just reading")
            self.fp.seek(0)
            # UNDONE -- so much for that buffer size thing.
            n, err = decoder.decode(self.fp.read())

        if fp:
            try:
                os.close(fp)
            except OSError:
                pass

        self.tile = []
        self.readonly = 0

        self.load_end()

        # libtiff closed the fp in a, we need to close self.fp, if possible
        if close_self_fp:
            self.fp.close()
            self.fp = None  # might be shared

        if err < 0:
            raise OSError(err)

        return Image.Image.load(self)

    def _setup(self):
        """Setup this image object based on current tags"""

        if 0xBC01 in self.tag_v2:
            raise OSError("Windows Media Photo files not yet supported")

        # extract relevant tags
        self._compression = COMPRESSION_INFO[self.tag_v2.get(COMPRESSION, 1)]
        self._planar_configuration = self.tag_v2.get(PLANAR_CONFIGURATION, 1)

        # photometric is a required tag, but not everyone is reading
        # the specification
        photo = self.tag_v2.get(PHOTOMETRIC_INTERPRETATION, 0)

        # old style jpeg compression images most certainly are YCbCr
        if self._compression == "tiff_jpeg":
            photo = 6

        fillorder = self.tag_v2.get(FILLORDER, 1)

        logger.debug("*** Summary ***")
        logger.debug(f"- compression: {self._compression}")
        logger.debug(f"- photometric_interpretation: {photo}")
        logger.debug(f"- planar_configuration: {self._planar_configuration}")
        logger.debug(f"- fill_order: {fillorder}")
        logger.debug(f"- YCbCr subsampling: {self.tag.get(YCBCRSUBSAMPLING)}")

        # size
        xsize = int(self.tag_v2.get(IMAGEWIDTH))
        ysize = int(self.tag_v2.get(IMAGELENGTH))
        self._size = xsize, ysize

        logger.debug(f"- size: {self.size}")

        sample_format = self.tag_v2.get(SAMPLEFORMAT, (1,))
        if len(sample_format) > 1 and max(sample_format) == min(sample_format) == 1:
            # SAMPLEFORMAT is properly per band, so an RGB image will
            # be (1,1,1).  But, we don't support per band pixel types,
            # and anything more than one band is a uint8. So, just
            # take the first element. Revisit this if adding support
            # for more exotic images.
            sample_format = (1,)

        bps_tuple = self.tag_v2.get(BITSPERSAMPLE, (1,))
        extra_tuple = self.tag_v2.get(EXTRASAMPLES, ())
        if photo in (2, 6, 8):  # RGB, YCbCr, LAB
            bps_count = 3
        elif photo == 5:  # CMYK
            bps_count = 4
        else:
            bps_count = 1
        bps_count += len(extra_tuple)
        bps_actual_count = len(bps_tuple)
        samples_per_pixel = self.tag_v2.get(
            SAMPLESPERPIXEL,
            3 if self._compression == "tiff_jpeg" and photo in (2, 6) else 1,
        )

        if samples_per_pixel > MAX_SAMPLESPERPIXEL:
            # DOS check, samples_per_pixel can be a Long, and we extend the tuple below
            logger.error(
                "More samples per pixel than can be decoded: %s", samples_per_pixel
            )
            raise SyntaxError("Invalid value for samples per pixel")

        if samples_per_pixel < bps_actual_count:
            # If a file has more values in bps_tuple than expected,
            # remove the excess.
            bps_tuple = bps_tuple[:samples_per_pixel]
        elif samples_per_pixel > bps_actual_count and bps_actual_count == 1:
            # If a file has only one value in bps_tuple, when it should have more,
            # presume it is the same number of bits for all of the samples.
            bps_tuple = bps_tuple * samples_per_pixel

        if len(bps_tuple) != samples_per_pixel:
            raise SyntaxError("unknown data organization")

        # mode: check photometric interpretation and bits per pixel
        key = (
            self.tag_v2.prefix,
            photo,
            sample_format,
            fillorder,
            bps_tuple,
            extra_tuple,
        )
        logger.debug(f"format key: {key}")
        try:
            self.mode, rawmode = OPEN_INFO[key]
        except KeyError as e:
            logger.debug("- unsupported format")
            raise SyntaxError("unknown pixel mode") from e

        logger.debug(f"- raw mode: {rawmode}")
        logger.debug(f"- pil mode: {self.mode}")

        self.info["compression"] = self._compression

        xres = self.tag_v2.get(X_RESOLUTION, 1)
        yres = self.tag_v2.get(Y_RESOLUTION, 1)

        if xres and yres:
            resunit = self.tag_v2.get(RESOLUTION_UNIT)
            if resunit == 2:  # dots per inch
                self.info["dpi"] = (xres, yres)
            elif resunit == 3:  # dots per centimeter. convert to dpi
                self.info["dpi"] = (xres * 2.54, yres * 2.54)
            elif resunit is None:  # used to default to 1, but now 2)
                self.info["dpi"] = (xres, yres)
                # For backward compatibility,
                # we also preserve the old behavior
                self.info["resolution"] = xres, yres
            else:  # No absolute unit of measurement
                self.info["resolution"] = xres, yres

        # build tile descriptors
        x = y = layer = 0
        self.tile = []
        self.use_load_libtiff = READ_LIBTIFF or self._compression != "raw"
        if self.use_load_libtiff:
            # Decoder expects entire file as one tile.
            # There's a buffer size limit in load (64k)
            # so large g4 images will fail if we use that
            # function.
            #
            # Setup the one tile for the whole image, then
            # use the _load_libtiff function.

            # libtiff handles the fillmode for us, so 1;IR should
            # actually be 1;I. Including the R double reverses the
            # bits, so stripes of the image are reversed.  See
            # https://github.com/python-pillow/Pillow/issues/279
            if fillorder == 2:
                # Replace fillorder with fillorder=1
                key = key[:3] + (1,) + key[4:]
                logger.debug(f"format key: {key}")
                # this should always work, since all the
                # fillorder==2 modes have a corresponding
                # fillorder=1 mode
                self.mode, rawmode = OPEN_INFO[key]
            # libtiff always returns the bytes in native order.
            # we're expecting image byte order. So, if the rawmode
            # contains I;16, we need to convert from native to image
            # byte order.
            if rawmode == "I;16":
                rawmode = "I;16N"
            if ";16B" in rawmode:
                rawmode = rawmode.replace(";16B", ";16N")
            if ";16L" in rawmode:
                rawmode = rawmode.replace(";16L", ";16N")

            # YCbCr images with new jpeg compression with pixels in one plane
            # unpacked straight into RGB values
            if (
                photo == 6
                and self._compression == "jpeg"
                and self._planar_configuration == 1
            ):
                rawmode = "RGB"

            # Offset in the tile tuple is 0, we go from 0,0 to
            # w,h, and we only do this once -- eds
            a = (rawmode, self._compression, False, self.tag_v2.offset)
            self.tile.append(("libtiff", (0, 0, xsize, ysize), 0, a))

        elif STRIPOFFSETS in self.tag_v2 or TILEOFFSETS in self.tag_v2:
            # striped image
            if STRIPOFFSETS in self.tag_v2:
                offsets = self.tag_v2[STRIPOFFSETS]
                h = self.tag_v2.get(ROWSPERSTRIP, ysize)
                w = self.size[0]
            else:
                # tiled image
                offsets = self.tag_v2[TILEOFFSETS]
                w = self.tag_v2.get(TILEWIDTH)
                h = self.tag_v2.get(TILELENGTH)

            for offset in offsets:
                if x + w > xsize:
                    stride = w * sum(bps_tuple) / 8  # bytes per line
                else:
                    stride = 0

                tile_rawmode = rawmode
                if self._planar_configuration == 2:
                    # each band on it's own layer
                    tile_rawmode = rawmode[layer]
                    # adjust stride width accordingly
                    stride /= bps_count

                a = (tile_rawmode, int(stride), 1)
                self.tile.append(
                    (
                        self._compression,
                        (x, y, min(x + w, xsize), min(y + h, ysize)),
                        offset,
                        a,
                    )
                )
                x = x + w
                if x >= self.size[0]:
                    x, y = 0, y + h
                    if y >= self.size[1]:
                        x = y = 0
                        layer += 1
        else:
            logger.debug("- unsupported data organization")
            raise SyntaxError("unknown data organization")

        # Fix up info.
        if ICCPROFILE in self.tag_v2:
            self.info["icc_profile"] = self.tag_v2[ICCPROFILE]

        # fixup palette descriptor

        if self.mode in ["P", "PA"]:
            palette = [o8(b // 256) for b in self.tag_v2[COLORMAP]]
            self.palette = ImagePalette.raw("RGB;L", b"".join(palette))

        self._tile_orientation = self.tag_v2.get(0x0112)


#
# --------------------------------------------------------------------
# Write TIFF files

# little endian is default except for image modes with
# explicit big endian byte-order

SAVE_INFO = {
    # mode => rawmode, byteorder, photometrics,
    #           sampleformat, bitspersample, extra
    "1": ("1", II, 1, 1, (1,), None),
    "L": ("L", II, 1, 1, (8,), None),
    "LA": ("LA", II, 1, 1, (8, 8), 2),
    "P": ("P", II, 3, 1, (8,), None),
    "PA": ("PA", II, 3, 1, (8, 8), 2),
    "I": ("I;32S", II, 1, 2, (32,), None),
    "I;16": ("I;16", II, 1, 1, (16,), None),
    "I;16S": ("I;16S", II, 1, 2, (16,), None),
    "F": ("F;32F", II, 1, 3, (32,), None),
    "RGB": ("RGB", II, 2, 1, (8, 8, 8), None),
    "RGBX": ("RGBX", II, 2, 1, (8, 8, 8, 8), 0),
    "RGBA": ("RGBA", II, 2, 1, (8, 8, 8, 8), 2),
    "CMYK": ("CMYK", II, 5, 1, (8, 8, 8, 8), None),
    "YCbCr": ("YCbCr", II, 6, 1, (8, 8, 8), None),
    "LAB": ("LAB", II, 8, 1, (8, 8, 8), None),
    "I;32BS": ("I;32BS", MM, 1, 2, (32,), None),
    "I;16B": ("I;16B", MM, 1, 1, (16,), None),
    "I;16BS": ("I;16BS", MM, 1, 2, (16,), None),
    "F;32BF": ("F;32BF", MM, 1, 3, (32,), None),
}


def _save(im, fp, filename):

    try:
        rawmode, prefix, photo, format, bits, extra = SAVE_INFO[im.mode]
    except KeyError as e:
        raise OSError(f"cannot write mode {im.mode} as TIFF") from e

    ifd = ImageFileDirectory_v2(prefix=prefix)

    encoderinfo = im.encoderinfo
    encoderconfig = im.encoderconfig
    try:
        compression = encoderinfo["compression"]
    except KeyError:
        compression = im.info.get("compression")
        if isinstance(compression, int):
            # compression value may be from BMP. Ignore it
            compression = None
    if compression is None:
        compression = "raw"
    elif compression == "tiff_jpeg":
        # OJPEG is obsolete, so use new-style JPEG compression instead
        compression = "jpeg"
    elif compression == "tiff_deflate":
        compression = "tiff_adobe_deflate"

    libtiff = WRITE_LIBTIFF or compression != "raw"

    # required for color libtiff images
    ifd[PLANAR_CONFIGURATION] = 1

    ifd[IMAGEWIDTH] = im.size[0]
    ifd[IMAGELENGTH] = im.size[1]

    # write any arbitrary tags passed in as an ImageFileDirectory
    if "tiffinfo" in encoderinfo:
        info = encoderinfo["tiffinfo"]
    elif "exif" in encoderinfo:
        info = encoderinfo["exif"]
        if isinstance(info, bytes):
            exif = Image.Exif()
            exif.load(info)
            info = exif
    else:
        info = {}
    logger.debug("Tiffinfo Keys: %s" % list(info))
    if isinstance(info, ImageFileDirectory_v1):
        info = info.to_v2()
    for key in info:
        if isinstance(info, Image.Exif) and key in TiffTags.TAGS_V2_GROUPS.keys():
            ifd[key] = info.get_ifd(key)
        else:
            ifd[key] = info.get(key)
        try:
            ifd.tagtype[key] = info.tagtype[key]
        except Exception:
            pass  # might not be an IFD. Might not have populated type

    # additions written by Greg Couch, gregc@cgl.ucsf.edu
    # inspired by image-sig posting from Kevin Cazabon, kcazabon@home.com
    if hasattr(im, "tag_v2"):
        # preserve tags from original TIFF image file
        for key in (
            RESOLUTION_UNIT,
            X_RESOLUTION,
            Y_RESOLUTION,
            IPTC_NAA_CHUNK,
            PHOTOSHOP_CHUNK,
            XMP,
        ):
            if key in im.tag_v2:
                ifd[key] = im.tag_v2[key]
                ifd.tagtype[key] = im.tag_v2.tagtype[key]

    # preserve ICC profile (should also work when saving other formats
    # which support profiles as TIFF) -- 2008-06-06 Florian Hoech
    icc = encoderinfo.get("icc_profile", im.info.get("icc_profile"))
    if icc:
        ifd[ICCPROFILE] = icc

    for key, name in [
        (IMAGEDESCRIPTION, "description"),
        (X_RESOLUTION, "resolution"),
        (Y_RESOLUTION, "resolution"),
        (X_RESOLUTION, "x_resolution"),
        (Y_RESOLUTION, "y_resolution"),
        (RESOLUTION_UNIT, "resolution_unit"),
        (SOFTWARE, "software"),
        (DATE_TIME, "date_time"),
        (ARTIST, "artist"),
        (COPYRIGHT, "copyright"),
    ]:
        if name in encoderinfo:
            ifd[key] = encoderinfo[name]

    dpi = encoderinfo.get("dpi")
    if dpi:
        ifd[RESOLUTION_UNIT] = 2
        ifd[X_RESOLUTION] = dpi[0]
        ifd[Y_RESOLUTION] = dpi[1]

    if bits != (1,):
        ifd[BITSPERSAMPLE] = bits
        if len(bits) != 1:
            ifd[SAMPLESPERPIXEL] = len(bits)
    if extra is not None:
        ifd[EXTRASAMPLES] = extra
    if format != 1:
        ifd[SAMPLEFORMAT] = format

    if PHOTOMETRIC_INTERPRETATION not in ifd:
        ifd[PHOTOMETRIC_INTERPRETATION] = photo
    elif im.mode in ("1", "L") and ifd[PHOTOMETRIC_INTERPRETATION] == 0:
        if im.mode == "1":
            inverted_im = im.copy()
            px = inverted_im.load()
            for y in range(inverted_im.height):
                for x in range(inverted_im.width):
                    px[x, y] = 0 if px[x, y] == 255 else 255
            im = inverted_im
        else:
            im = ImageOps.invert(im)

    if im.mode in ["P", "PA"]:
        lut = im.im.getpalette("RGB", "RGB;L")
        colormap = []
        colors = len(lut) // 3
        for i in range(3):
            colormap += [v * 256 for v in lut[colors * i : colors * (i + 1)]]
            colormap += [0] * (256 - colors)
        ifd[COLORMAP] = colormap
    # data orientation
    stride = len(bits) * ((im.size[0] * bits[0] + 7) // 8)
    # aim for given strip size (64 KB by default) when using libtiff writer
    if libtiff:
        im_strip_size = encoderinfo.get("strip_size", STRIP_SIZE)
        rows_per_strip = 1 if stride == 0 else min(im_strip_size // stride, im.size[1])
        # JPEG encoder expects multiple of 8 rows
        if compression == "jpeg":
            rows_per_strip = min(((rows_per_strip + 7) // 8) * 8, im.size[1])
    else:
        rows_per_strip = im.size[1]
    if rows_per_strip == 0:
        rows_per_strip = 1
    strip_byte_counts = 1 if stride == 0 else stride * rows_per_strip
    strips_per_image = (im.size[1] + rows_per_strip - 1) // rows_per_strip
    ifd[ROWSPERSTRIP] = rows_per_strip
    if strip_byte_counts >= 2**16:
        ifd.tagtype[STRIPBYTECOUNTS] = TiffTags.LONG
    ifd[STRIPBYTECOUNTS] = (strip_byte_counts,) * (strips_per_image - 1) + (
        stride * im.size[1] - strip_byte_counts * (strips_per_image - 1),
    )
    ifd[STRIPOFFSETS] = tuple(
        range(0, strip_byte_counts * strips_per_image, strip_byte_counts)
    )  # this is adjusted by IFD writer
    # no compression by default:
    ifd[COMPRESSION] = COMPRESSION_INFO_REV.get(compression, 1)

    if im.mode == "YCbCr":
        for tag, value in {
            YCBCRSUBSAMPLING: (1, 1),
            REFERENCEBLACKWHITE: (0, 255, 128, 255, 128, 255),
        }.items():
            ifd.setdefault(tag, value)

    blocklist = [TILEWIDTH, TILELENGTH, TILEOFFSETS, TILEBYTECOUNTS]
    if libtiff:
        if "quality" in encoderinfo:
            quality = encoderinfo["quality"]
            if not isinstance(quality, int) or quality < 0 or quality > 100:
                raise ValueError("Invalid quality setting")
            if compression != "jpeg":
                raise ValueError(
                    "quality setting only supported for 'jpeg' compression"
                )
            ifd[JPEGQUALITY] = quality

        logger.debug("Saving using libtiff encoder")
        logger.debug("Items: %s" % sorted(ifd.items()))
        _fp = 0
        if hasattr(fp, "fileno"):
            try:
                fp.seek(0)
                _fp = os.dup(fp.fileno())
            except io.UnsupportedOperation:
                pass

        # optional types for non core tags
        types = {}
        # STRIPOFFSETS and STRIPBYTECOUNTS are added by the library
        # based on the data in the strip.
        # The other tags expect arrays with a certain length (fixed or depending on
        # BITSPERSAMPLE, etc), passing arrays with a different length will result in
        # segfaults. Block these tags until we add extra validation.
        # SUBIFD may also cause a segfault.
        blocklist += [
            REFERENCEBLACKWHITE,
            STRIPBYTECOUNTS,
            STRIPOFFSETS,
            TRANSFERFUNCTION,
            SUBIFD,
        ]

        # bits per sample is a single short in the tiff directory, not a list.
        atts = {BITSPERSAMPLE: bits[0]}
        # Merge the ones that we have with (optional) more bits from
        # the original file, e.g x,y resolution so that we can
        # save(load('')) == original file.
        legacy_ifd = {}
        if hasattr(im, "tag"):
            legacy_ifd = im.tag.to_v2()

        # SAMPLEFORMAT is determined by the image format and should not be copied
        # from legacy_ifd.
        supplied_tags = {**getattr(im, "tag_v2", {}), **legacy_ifd}
        if SAMPLEFORMAT in supplied_tags:
            del supplied_tags[SAMPLEFORMAT]

        for tag, value in itertools.chain(ifd.items(), supplied_tags.items()):
            # Libtiff can only process certain core items without adding
            # them to the custom dictionary.
            # Custom items are supported for int, float, unicode, string and byte
            # values. Other types and tuples require a tagtype.
            if tag not in TiffTags.LIBTIFF_CORE:
                if not Image.core.libtiff_support_custom_tags:
                    continue

                if tag in ifd.tagtype:
                    types[tag] = ifd.tagtype[tag]
                elif not (isinstance(value, (int, float, str, bytes))):
                    continue
                else:
                    type = TiffTags.lookup(tag).type
                    if type:
                        types[tag] = type
            if tag not in atts and tag not in blocklist:
                if isinstance(value, str):
                    atts[tag] = value.encode("ascii", "replace") + b"\0"
                elif isinstance(value, IFDRational):
                    atts[tag] = float(value)
                else:
                    atts[tag] = value

        if SAMPLEFORMAT in atts and len(atts[SAMPLEFORMAT]) == 1:
            atts[SAMPLEFORMAT] = atts[SAMPLEFORMAT][0]

        logger.debug("Converted items: %s" % sorted(atts.items()))

        # libtiff always expects the bytes in native order.
        # we're storing image byte order. So, if the rawmode
        # contains I;16, we need to convert from native to image
        # byte order.
        if im.mode in ("I;16B", "I;16"):
            rawmode = "I;16N"

        # Pass tags as sorted list so that the tags are set in a fixed order.
        # This is required by libtiff for some tags. For example, the JPEGQUALITY
        # pseudo tag requires that the COMPRESS tag was already set.
        tags = list(atts.items())
        tags.sort()
        a = (rawmode, compression, _fp, filename, tags, types)
        e = Image._getencoder(im.mode, "libtiff", a, encoderconfig)
        e.setimage(im.im, (0, 0) + im.size)
        while True:
            # undone, change to self.decodermaxblock:
            l, s, d = e.encode(16 * 1024)
            if not _fp:
                fp.write(d)
            if s:
                break
        if s < 0:
            raise OSError(f"encoder error {s} when writing image file")

    else:
        for tag in blocklist:
            del ifd[tag]
        offset = ifd.save(fp)

        ImageFile._save(
            im, fp, [("raw", (0, 0) + im.size, offset, (rawmode, stride, 1))]
        )

    # -- helper for multi-page save --
    if "_debug_multipage" in encoderinfo:
        # just to access o32 and o16 (using correct byte order)
        im._debug_multipage = ifd


class AppendingTiffWriter:
    fieldSizes = [
        0,  # None
        1,  # byte
        1,  # ascii
        2,  # short
        4,  # long
        8,  # rational
        1,  # sbyte
        1,  # undefined
        2,  # sshort
        4,  # slong
        8,  # srational
        4,  # float
        8,  # double
    ]

    #    StripOffsets = 273
    #    FreeOffsets = 288
    #    TileOffsets = 324
    #    JPEGQTables = 519
    #    JPEGDCTables = 520
    #    JPEGACTables = 521
    Tags = {273, 288, 324, 519, 520, 521}

    def __init__(self, fn, new=False):
        if hasattr(fn, "read"):
            self.f = fn
            self.close_fp = False
        else:
            self.name = fn
            self.close_fp = True
            try:
                self.f = open(fn, "w+b" if new else "r+b")
            except OSError:
                self.f = open(fn, "w+b")
        self.beginning = self.f.tell()
        self.setup()

    def setup(self):
        # Reset everything.
        self.f.seek(self.beginning, os.SEEK_SET)

        self.whereToWriteNewIFDOffset = None
        self.offsetOfNewPage = 0

        self.IIMM = iimm = self.f.read(4)
        if not iimm:
            # empty file - first page
            self.isFirst = True
            return

        self.isFirst = False
        if iimm == b"II\x2a\x00":
            self.setEndian("<")
        elif iimm == b"MM\x00\x2a":
            self.setEndian(">")
        else:
            raise RuntimeError("Invalid TIFF file header")

        self.skipIFDs()
        self.goToEnd()

    def finalize(self):
        if self.isFirst:
            return

        # fix offsets
        self.f.seek(self.offsetOfNewPage)

        iimm = self.f.read(4)
        if not iimm:
            # raise RuntimeError("nothing written into new page")
            # Make it easy to finish a frame without committing to a new one.
            return

        if iimm != self.IIMM:
            raise RuntimeError("IIMM of new page doesn't match IIMM of first page")

        ifd_offset = self.readLong()
        ifd_offset += self.offsetOfNewPage
        self.f.seek(self.whereToWriteNewIFDOffset)
        self.writeLong(ifd_offset)
        self.f.seek(ifd_offset)
        self.fixIFD()

    def newFrame(self):
        # Call this to finish a frame.
        self.finalize()
        self.setup()

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc_value, traceback):
        if self.close_fp:
            self.close()
        return False

    def tell(self):
        return self.f.tell() - self.offsetOfNewPage

    def seek(self, offset, whence=io.SEEK_SET):
        if whence == os.SEEK_SET:
            offset += self.offsetOfNewPage

        self.f.seek(offset, whence)
        return self.tell()

    def goToEnd(self):
        self.f.seek(0, os.SEEK_END)
        pos = self.f.tell()

        # pad to 16 byte boundary
        pad_bytes = 16 - pos % 16
        if 0 < pad_bytes < 16:
            self.f.write(bytes(pad_bytes))
        self.offsetOfNewPage = self.f.tell()

    def setEndian(self, endian):
        self.endian = endian
        self.longFmt = self.endian + "L"
        self.shortFmt = self.endian + "H"
        self.tagFormat = self.endian + "HHL"

    def skipIFDs(self):
        while True:
            ifd_offset = self.readLong()
            if ifd_offset == 0:
                self.whereToWriteNewIFDOffset = self.f.tell() - 4
                break

            self.f.seek(ifd_offset)
            num_tags = self.readShort()
            self.f.seek(num_tags * 12, os.SEEK_CUR)

    def write(self, data):
        return self.f.write(data)

    def readShort(self):
        (value,) = struct.unpack(self.shortFmt, self.f.read(2))
        return value

    def readLong(self):
        (value,) = struct.unpack(self.longFmt, self.f.read(4))
        return value

    def rewriteLastShortToLong(self, value):
        self.f.seek(-2, os.SEEK_CUR)
        bytes_written = self.f.write(struct.pack(self.longFmt, value))
        if bytes_written is not None and bytes_written != 4:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")

    def rewriteLastShort(self, value):
        self.f.seek(-2, os.SEEK_CUR)
        bytes_written = self.f.write(struct.pack(self.shortFmt, value))
        if bytes_written is not None and bytes_written != 2:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")

    def rewriteLastLong(self, value):
        self.f.seek(-4, os.SEEK_CUR)
        bytes_written = self.f.write(struct.pack(self.longFmt, value))
        if bytes_written is not None and bytes_written != 4:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")

    def writeShort(self, value):
        bytes_written = self.f.write(struct.pack(self.shortFmt, value))
        if bytes_written is not None and bytes_written != 2:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 2")

    def writeLong(self, value):
        bytes_written = self.f.write(struct.pack(self.longFmt, value))
        if bytes_written is not None and bytes_written != 4:
            raise RuntimeError(f"wrote only {bytes_written} bytes but wanted 4")

    def close(self):
        self.finalize()
        self.f.close()

    def fixIFD(self):
        num_tags = self.readShort()

        for i in range(num_tags):
            tag, field_type, count = struct.unpack(self.tagFormat, self.f.read(8))

            field_size = self.fieldSizes[field_type]
            total_size = field_size * count
            is_local = total_size <= 4
            if not is_local:
                offset = self.readLong()
                offset += self.offsetOfNewPage
                self.rewriteLastLong(offset)

            if tag in self.Tags:
                cur_pos = self.f.tell()

                if is_local:
                    self.fixOffsets(
                        count, isShort=(field_size == 2), isLong=(field_size == 4)
                    )
                    self.f.seek(cur_pos + 4)
                else:
                    self.f.seek(offset)
                    self.fixOffsets(
                        count, isShort=(field_size == 2), isLong=(field_size == 4)
                    )
                    self.f.seek(cur_pos)

                offset = cur_pos = None

            elif is_local:
                # skip the locally stored value that is not an offset
                self.f.seek(4, os.SEEK_CUR)

    def fixOffsets(self, count, isShort=False, isLong=False):
        if not isShort and not isLong:
            raise RuntimeError("offset is neither short nor long")

        for i in range(count):
            offset = self.readShort() if isShort else self.readLong()
            offset += self.offsetOfNewPage
            if isShort and offset >= 65536:
                # offset is now too large - we must convert shorts to longs
                if count != 1:
                    raise RuntimeError("not implemented")  # XXX TODO

                # simple case - the offset is just one and therefore it is
                # local (not referenced with another offset)
                self.rewriteLastShortToLong(offset)
                self.f.seek(-10, os.SEEK_CUR)
                self.writeShort(TiffTags.LONG)  # rewrite the type to LONG
                self.f.seek(8, os.SEEK_CUR)
            elif isShort:
                self.rewriteLastShort(offset)
            else:
                self.rewriteLastLong(offset)


def _save_all(im, fp, filename):
    encoderinfo = im.encoderinfo.copy()
    encoderconfig = im.encoderconfig
    append_images = list(encoderinfo.get("append_images", []))
    if not hasattr(im, "n_frames") and not append_images:
        return _save(im, fp, filename)

    cur_idx = im.tell()
    try:
        with AppendingTiffWriter(fp) as tf:
            for ims in [im] + append_images:
                ims.encoderinfo = encoderinfo
                ims.encoderconfig = encoderconfig
                if not hasattr(ims, "n_frames"):
                    nfr = 1
                else:
                    nfr = ims.n_frames

                for idx in range(nfr):
                    ims.seek(idx)
                    ims.load()
                    _save(ims, tf, filename)
                    tf.newFrame()
    finally:
        im.seek(cur_idx)


#
# --------------------------------------------------------------------
# Register

Image.register_open(TiffImageFile.format, TiffImageFile, _accept)
Image.register_save(TiffImageFile.format, _save)
Image.register_save_all(TiffImageFile.format, _save_all)

Image.register_extensions(TiffImageFile.format, [".tif", ".tiff"])

Image.register_mime(TiffImageFile.format, "image/tiff")

```
--- END FILE ---

Please first localize the bug based on the issue statement, and then generate *SEARCH/REPLACE* edits to fix the issue.

Every *SEARCH/REPLACE* edit must use this format:
1. The file path
2. The start of search block: <<<<<<< SEARCH
3. A contiguous chunk of lines to search for in the existing source code
4. The dividing line: =======
5. The lines to replace into the source code
6. The end of the replace block: >>>>>>> REPLACE

Here is an example:

```python
### mathweb/flask/app.py
<<<<<<< SEARCH
from flask import Flask
=======
import math
from flask import Flask
>>>>>>> REPLACE
```
</solution>

Please note that the *SEARCH/REPLACE* edit REQUIRES PROPER INDENTATION. If you would like to add the line '        print(x)', you must fully write that out, with all those spaces before the code!
Wrap each *SEARCH/REPLACE* edit in a code block as shown in the example above. If you have multiple *SEARCH/REPLACE* edits, use a separate code block for each one.
user