{
    "file_diffs": [
        {
            "old_file_content": "from __future__ import annotations\n\nimport sys\nfrom io import BytesIO\nfrom pathlib import Path\n\nimport pytest\n\nfrom PIL import Image, PpmImagePlugin\n\nfrom .helper import (\n    assert_image_equal,\n    assert_image_equal_tofile,\n    assert_image_similar,\n    hopper,\n)\n\n# sample ppm stream\nTEST_FILE = \"Tests/images/hopper.ppm\"\n\n\ndef test_sanity() -> None:\n    with Image.open(TEST_FILE) as im:\n        assert im.mode == \"RGB\"\n        assert im.size == (128, 128)\n        assert im.format == \"PPM\"\n        assert im.get_format_mimetype() == \"image/x-portable-pixmap\"\n\n\n@pytest.mark.parametrize(\n    \"data, mode, pixels\",\n    (\n        (b\"P2 3 1 4 0 2 4\", \"L\", (0, 128, 255)),\n        (b\"P2 3 1 257 0 128 257\", \"I\", (0, 32640, 65535)),\n        # P3 with maxval < 255\n        (\n            b\"P3 3 1 17 0 1 2 8 9 10 15 16 17\",\n            \"RGB\",\n            ((0, 15, 30), (120, 135, 150), (225, 240, 255)),\n        ),\n        # P3 with maxval > 255\n        # Scale down to 255, since there is no RGB mode with more than 8-bit\n        (\n            b\"P3 3 1 257 0 1 2 128 129 130 256 257 257\",\n            \"RGB\",\n            ((0, 1, 2), (127, 128, 129), (254, 255, 255)),\n        ),\n        (b\"P5 3 1 4 \\x00\\x02\\x04\", \"L\", (0, 128, 255)),\n        (b\"P5 3 1 257 \\x00\\x00\\x00\\x80\\x01\\x01\", \"I\", (0, 32640, 65535)),\n        # P6 with maxval < 255\n        (\n            b\"P6 3 1 17 \\x00\\x01\\x02\\x08\\x09\\x0A\\x0F\\x10\\x11\",\n            \"RGB\",\n            (\n                (0, 15, 30),\n                (120, 135, 150),\n                (225, 240, 255),\n            ),\n        ),\n        # P6 with maxval > 255\n        (\n            b\"P6 3 1 257 \\x00\\x00\\x00\\x01\\x00\\x02\"\n            b\"\\x00\\x80\\x00\\x81\\x00\\x82\\x01\\x00\\x01\\x01\\xFF\\xFF\",\n            \"RGB\",\n            (\n                (0, 1, 2),\n                (127, 128, 129),\n                (254, 255, 255),\n            ),\n        ),\n    ),\n)\ndef test_arbitrary_maxval(\n    data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...]\n) -> None:\n    fp = BytesIO(data)\n    with Image.open(fp) as im:\n        assert im.size == (3, 1)\n        assert im.mode == mode\n\n        px = im.load()\n        assert tuple(px[x, 0] for x in range(3)) == pixels\n\n\ndef test_16bit_pgm() -> None:\n    with Image.open(\"Tests/images/16_bit_binary.pgm\") as im:\n        assert im.mode == \"I\"\n        assert im.size == (20, 100)\n        assert im.get_format_mimetype() == \"image/x-portable-graymap\"\n\n        assert_image_equal_tofile(im, \"Tests/images/16_bit_binary_pgm.tiff\")\n\n\ndef test_16bit_pgm_write(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/16_bit_binary.pgm\") as im:\n        filename = str(tmp_path / \"temp.pgm\")\n        im.save(filename, \"PPM\")\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pnm(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper.pnm\") as im:\n        assert_image_similar(im, hopper(), 0.0001)\n\n        filename = str(tmp_path / \"temp.pnm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pfm(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper.pfm\") as im:\n        assert im.info[\"scale\"] == 1.0\n        assert_image_equal(im, hopper(\"F\"))\n\n        filename = str(tmp_path / \"tmp.pfm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pfm_big_endian(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper_be.pfm\") as im:\n        assert im.info[\"scale\"] == 2.5\n        assert_image_equal(im, hopper(\"F\"))\n\n        filename = str(tmp_path / \"tmp.pfm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\n@pytest.mark.parametrize(\n    \"data\",\n    [\n        b\"Pf 1 1 NaN \\0\\0\\0\\0\",\n        b\"Pf 1 1 inf \\0\\0\\0\\0\",\n        b\"Pf 1 1 -inf \\0\\0\\0\\0\",\n        b\"Pf 1 1 0.0 \\0\\0\\0\\0\",\n        b\"Pf 1 1 -0.0 \\0\\0\\0\\0\",\n    ],\n)\ndef test_pfm_invalid(data: bytes) -> None:\n    with pytest.raises(ValueError):\n        with Image.open(BytesIO(data)):\n            pass\n\n\n@pytest.mark.parametrize(\n    \"plain_path, raw_path\",\n    (\n        (\n            \"Tests/images/hopper_1bit_plain.pbm\",  # P1\n            \"Tests/images/hopper_1bit.pbm\",  # P4\n        ),\n        (\n            \"Tests/images/hopper_8bit_plain.pgm\",  # P2\n            \"Tests/images/hopper_8bit.pgm\",  # P5\n        ),\n        (\n            \"Tests/images/hopper_8bit_plain.ppm\",  # P3\n            \"Tests/images/hopper_8bit.ppm\",  # P6\n        ),\n    ),\n)\ndef test_plain(plain_path: str, raw_path: str) -> None:\n    with Image.open(plain_path) as im:\n        assert_image_equal_tofile(im, raw_path)\n\n\ndef test_16bit_plain_pgm() -> None:\n    # P2 with maxval 2 ** 16 - 1\n    with Image.open(\"Tests/images/hopper_16bit_plain.pgm\") as im:\n        assert im.mode == \"I\"\n        assert im.size == (128, 128)\n        assert im.get_format_mimetype() == \"image/x-portable-graymap\"\n\n        # P5 with maxval 2 ** 16 - 1\n        assert_image_equal_tofile(im, \"Tests/images/hopper_16bit.pgm\")\n\n\n@pytest.mark.parametrize(\n    \"header, data, comment_count\",\n    (\n        (b\"P1\\n2 2\", b\"1010\", 10**6),\n        (b\"P2\\n3 1\\n4\", b\"0 2 4\", 1),\n        (b\"P3\\n2 2\\n255\", b\"0 0 0 001 1 1 2 2 2 255 255 255\", 10**6),\n    ),\n)\ndef test_plain_data_with_comment(\n    tmp_path: Path, header: bytes, data: bytes, comment_count: int\n) -> None:\n    path1 = str(tmp_path / \"temp1.ppm\")\n    path2 = str(tmp_path / \"temp2.ppm\")\n    comment = b\"# comment\" * comment_count\n    with open(path1, \"wb\") as f1, open(path2, \"wb\") as f2:\n        f1.write(header + b\"\\n\\n\" + data)\n        f2.write(header + b\"\\n\" + comment + b\"\\n\" + data + comment)\n\n    with Image.open(path1) as im:\n        assert_image_equal_tofile(im, path2)\n\n\n@pytest.mark.parametrize(\"data\", (b\"P1\\n128 128\\n\", b\"P3\\n128 128\\n255\\n\"))\ndef test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\"data\", (b\"P1\\n128 128\\n1009\", b\"P3\\n128 128\\n255\\n100A\"))\ndef test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\n    \"data\",\n    (\n        b\"P3\\n128 128\\n255\\n012345678910\",  # half token too long\n        b\"P3\\n128 128\\n255\\n012345678910 0\",  # token too long\n    ),\n)\ndef test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_plain_ppm_value_too_large(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n256\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_magic() -> None:\n    with pytest.raises(SyntaxError):\n        PpmImagePlugin.PpmImageFile(fp=BytesIO(b\"PyInvalid\"))\n\n\ndef test_header_with_comments(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6 #comment\\n#comment\\r12#comment\\r8\\n128 #comment\\n255\\n\")\n\n    with Image.open(path) as im:\n        assert im.size == (128, 128)\n\n\ndef test_non_integer_token(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\nTEST\")\n\n    with pytest.raises(ValueError):\n        with Image.open(path):\n            pass\n\n\ndef test_header_token_too_long(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\n 01234567890\")\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"Token too long in file header: 01234567890\"\n\n\ndef test_truncated_file(tmp_path: Path) -> None:\n    # Test EOF in header\n    path = str(tmp_path / \"temp.pgm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\")\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"Reached EOF while reading header\"\n\n    # Test EOF for PyDecoder\n    fp = BytesIO(b\"P5 3 1 4\")\n    with Image.open(fp) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_not_enough_image_data(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P2 1 2 255 255\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\"maxval\", (b\"0\", b\"65536\"))\ndef test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\n3 1 \" + maxval)\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"maxval must be greater than 0 and less than 65536\"\n\n\ndef test_neg_ppm() -> None:\n    # Storage.c accepted negative values for xsize, ysize.  the\n    # internal open_ppm function didn't check for sanity but it\n    # has been removed. The default opener doesn't accept negative\n    # sizes.\n\n    with pytest.raises(OSError):\n        with Image.open(\"Tests/images/negative_size.ppm\"):\n            pass\n\n\ndef test_mimetypes(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.pgm\")\n\n    with open(path, \"wb\") as f:\n        f.write(b\"P4\\n128 128\\n255\")\n    with Image.open(path) as im:\n        assert im.get_format_mimetype() == \"image/x-portable-bitmap\"\n\n    with open(path, \"wb\") as f:\n        f.write(b\"PyCMYK\\n128 128\\n255\")\n    with Image.open(path) as im:\n        assert im.get_format_mimetype() == \"image/x-portable-anymap\"\n\n\n@pytest.mark.parametrize(\"buffer\", (True, False))\ndef test_save_stdout(buffer: bool) -> None:\n    old_stdout = sys.stdout\n\n    if buffer:\n\n        class MyStdOut:\n            buffer = BytesIO()\n\n        mystdout = MyStdOut()\n    else:\n        mystdout = BytesIO()\n\n    sys.stdout = mystdout\n\n    with Image.open(TEST_FILE) as im:\n        im.save(sys.stdout, \"PPM\")\n\n    # Reset stdout\n    sys.stdout = old_stdout\n\n    if buffer:\n        mystdout = mystdout.buffer\n    with Image.open(mystdout) as reloaded:\n        assert_image_equal_tofile(reloaded, TEST_FILE)\n",
            "new_file_content": "from __future__ import annotations\n\nimport sys\nfrom io import BytesIO\nfrom pathlib import Path\n\nimport pytest\n\nfrom PIL import Image, PpmImagePlugin\n\nfrom .helper import (\n    assert_image_equal,\n    assert_image_equal_tofile,\n    assert_image_similar,\n    hopper,\n)\n\n# sample ppm stream\nTEST_FILE = \"Tests/images/hopper.ppm\"\n\n\ndef test_sanity() -> None:\n    with Image.open(TEST_FILE) as im:\n        assert im.mode == \"RGB\"\n        assert im.size == (128, 128)\n        assert im.format == \"PPM\"\n        assert im.get_format_mimetype() == \"image/x-portable-pixmap\"\n\n\n@pytest.mark.parametrize(\n    \"data, mode, pixels\",\n    (\n        (b\"P2 3 1 4 0 2 4\", \"L\", (0, 128, 255)),\n        (b\"P2 3 1 257 0 128 257\", \"I\", (0, 32640, 65535)),\n        # P3 with maxval < 255\n        (\n            b\"P3 3 1 17 0 1 2 8 9 10 15 16 17\",\n            \"RGB\",\n            ((0, 15, 30), (120, 135, 150), (225, 240, 255)),\n        ),\n        # P3 with maxval > 255\n        # Scale down to 255, since there is no RGB mode with more than 8-bit\n        (\n            b\"P3 3 1 257 0 1 2 128 129 130 256 257 257\",\n            \"RGB\",\n            ((0, 1, 2), (127, 128, 129), (254, 255, 255)),\n        ),\n        (b\"P5 3 1 4 \\x00\\x02\\x04\", \"L\", (0, 128, 255)),\n        (b\"P5 3 1 257 \\x00\\x00\\x00\\x80\\x01\\x01\", \"I\", (0, 32640, 65535)),\n        # P6 with maxval < 255\n        (\n            b\"P6 3 1 17 \\x00\\x01\\x02\\x08\\x09\\x0A\\x0F\\x10\\x11\",\n            \"RGB\",\n            (\n                (0, 15, 30),\n                (120, 135, 150),\n                (225, 240, 255),\n            ),\n        ),\n        # P6 with maxval > 255\n        (\n            b\"P6 3 1 257 \\x00\\x00\\x00\\x01\\x00\\x02\"\n            b\"\\x00\\x80\\x00\\x81\\x00\\x82\\x01\\x00\\x01\\x01\\xFF\\xFF\",\n            \"RGB\",\n            (\n                (0, 1, 2),\n                (127, 128, 129),\n                (254, 255, 255),\n            ),\n        ),\n    ),\n)\ndef test_arbitrary_maxval(\n    data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...]\n) -> None:\n    fp = BytesIO(data)\n    with Image.open(fp) as im:\n        assert im.size == (3, 1)\n        assert im.mode == mode\n\n        px = im.load()\n        assert tuple(px[x, 0] for x in range(3)) == pixels\n\n\ndef test_16bit_pgm() -> None:\n    with Image.open(\"Tests/images/16_bit_binary.pgm\") as im:\n        assert im.mode == \"I\"\n        assert im.size == (20, 100)\n        assert im.get_format_mimetype() == \"image/x-portable-graymap\"\n\n        assert_image_equal_tofile(im, \"Tests/images/16_bit_binary_pgm.tiff\")\n\n\ndef test_16bit_pgm_write(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/16_bit_binary.pgm\") as im:\n        filename = str(tmp_path / \"temp.pgm\")\n        im.save(filename, \"PPM\")\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pnm(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper.pnm\") as im:\n        assert_image_similar(im, hopper(), 0.0001)\n\n        filename = str(tmp_path / \"temp.pnm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pfm(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper.pfm\") as im:\n        assert im.info[\"scale\"] == 1.0\n        assert_image_equal(im, hopper(\"F\"))\n\n        filename = str(tmp_path / \"tmp.pfm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pfm_big_endian(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper_be.pfm\") as im:\n        assert im.info[\"scale\"] == 2.5\n        assert_image_equal(im, hopper(\"F\"))\n\n        filename = str(tmp_path / \"tmp.pfm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\n@pytest.mark.parametrize(\n    \"data\",\n    [\n        b\"Pf 1 1 NaN \\0\\0\\0\\0\",\n        b\"Pf 1 1 inf \\0\\0\\0\\0\",\n        b\"Pf 1 1 -inf \\0\\0\\0\\0\",\n        b\"Pf 1 1 0.0 \\0\\0\\0\\0\",\n        b\"Pf 1 1 -0.0 \\0\\0\\0\\0\",\n    ],\n)\ndef test_pfm_invalid(data: bytes) -> None:\n    with pytest.raises(ValueError):\n        with Image.open(BytesIO(data)):\n            pass\n\n\n@pytest.mark.parametrize(\n    \"plain_path, raw_path\",\n    (\n        (\n            \"Tests/images/hopper_1bit_plain.pbm\",  # P1\n            \"Tests/images/hopper_1bit.pbm\",  # P4\n        ),\n        (\n            \"Tests/images/hopper_8bit_plain.pgm\",  # P2\n            \"Tests/images/hopper_8bit.pgm\",  # P5\n        ),\n        (\n            \"Tests/images/hopper_8bit_plain.ppm\",  # P3\n            \"Tests/images/hopper_8bit.ppm\",  # P6\n        ),\n    ),\n)\ndef test_plain(plain_path: str, raw_path: str) -> None:\n    with Image.open(plain_path) as im:\n        assert_image_equal_tofile(im, raw_path)\n\n\ndef test_16bit_plain_pgm() -> None:\n    # P2 with maxval 2 ** 16 - 1\n    with Image.open(\"Tests/images/hopper_16bit_plain.pgm\") as im:\n        assert im.mode == \"I\"\n        assert im.size == (128, 128)\n        assert im.get_format_mimetype() == \"image/x-portable-graymap\"\n\n        # P5 with maxval 2 ** 16 - 1\n        assert_image_equal_tofile(im, \"Tests/images/hopper_16bit.pgm\")\n\n\n@pytest.mark.parametrize(\n    \"header, data, comment_count\",\n    (\n        (b\"P1\\n2 2\", b\"1010\", 10**6),\n        (b\"P2\\n3 1\\n4\", b\"0 2 4\", 1),\n        (b\"P3\\n2 2\\n255\", b\"0 0 0 001 1 1 2 2 2 255 255 255\", 10**6),\n    ),\n)\ndef test_plain_data_with_comment(\n    tmp_path: Path, header: bytes, data: bytes, comment_count: int\n) -> None:\n    path1 = str(tmp_path / \"temp1.ppm\")\n    path2 = str(tmp_path / \"temp2.ppm\")\n    comment = b\"# comment\" * comment_count\n    with open(path1, \"wb\") as f1, open(path2, \"wb\") as f2:\n        f1.write(header + b\"\\n\\n\" + data)\n        f2.write(header + b\"\\n\" + comment + b\"\\n\" + data + comment)\n\n    with Image.open(path1) as im:\n        assert_image_equal_tofile(im, path2)\n\n\n@pytest.mark.parametrize(\"data\", (b\"P1\\n128 128\\n\", b\"P3\\n128 128\\n255\\n\"))\ndef test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\"data\", (b\"P1\\n128 128\\n1009\", b\"P3\\n128 128\\n255\\n100A\"))\ndef test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\n    \"data\",\n    (\n        b\"P3\\n128 128\\n255\\n012345678910\",  # half token too long\n        b\"P3\\n128 128\\n255\\n012345678910 0\",  # token too long\n    ),\n)\ndef test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_plain_ppm_value_negative(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n-1\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError, match=\"Channel value is negative\"):\n            im.load()\n\n\ndef test_plain_ppm_value_too_large(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n256\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError, match=\"Channel value too large\"):\n            im.load()\n\n\ndef test_magic() -> None:\n    with pytest.raises(SyntaxError):\n        PpmImagePlugin.PpmImageFile(fp=BytesIO(b\"PyInvalid\"))\n\n\ndef test_header_with_comments(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6 #comment\\n#comment\\r12#comment\\r8\\n128 #comment\\n255\\n\")\n\n    with Image.open(path) as im:\n        assert im.size == (128, 128)\n\n\ndef test_non_integer_token(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\nTEST\")\n\n    with pytest.raises(ValueError):\n        with Image.open(path):\n            pass\n\n\ndef test_header_token_too_long(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\n 01234567890\")\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"Token too long in file header: 01234567890\"\n\n\ndef test_truncated_file(tmp_path: Path) -> None:\n    # Test EOF in header\n    path = str(tmp_path / \"temp.pgm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\")\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"Reached EOF while reading header\"\n\n    # Test EOF for PyDecoder\n    fp = BytesIO(b\"P5 3 1 4\")\n    with Image.open(fp) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_not_enough_image_data(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P2 1 2 255 255\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\"maxval\", (b\"0\", b\"65536\"))\ndef test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\n3 1 \" + maxval)\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"maxval must be greater than 0 and less than 65536\"\n\n\ndef test_neg_ppm() -> None:\n    # Storage.c accepted negative values for xsize, ysize.  the\n    # internal open_ppm function didn't check for sanity but it\n    # has been removed. The default opener doesn't accept negative\n    # sizes.\n\n    with pytest.raises(OSError):\n        with Image.open(\"Tests/images/negative_size.ppm\"):\n            pass\n\n\ndef test_mimetypes(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.pgm\")\n\n    with open(path, \"wb\") as f:\n        f.write(b\"P4\\n128 128\\n255\")\n    with Image.open(path) as im:\n        assert im.get_format_mimetype() == \"image/x-portable-bitmap\"\n\n    with open(path, \"wb\") as f:\n        f.write(b\"PyCMYK\\n128 128\\n255\")\n    with Image.open(path) as im:\n        assert im.get_format_mimetype() == \"image/x-portable-anymap\"\n\n\n@pytest.mark.parametrize(\"buffer\", (True, False))\ndef test_save_stdout(buffer: bool) -> None:\n    old_stdout = sys.stdout\n\n    if buffer:\n\n        class MyStdOut:\n            buffer = BytesIO()\n\n        mystdout = MyStdOut()\n    else:\n        mystdout = BytesIO()\n\n    sys.stdout = mystdout\n\n    with Image.open(TEST_FILE) as im:\n        im.save(sys.stdout, \"PPM\")\n\n    # Reset stdout\n    sys.stdout = old_stdout\n\n    if buffer:\n        mystdout = mystdout.buffer\n    with Image.open(mystdout) as reloaded:\n        assert_image_equal_tofile(reloaded, TEST_FILE)\n",
            "header": {
                "file": {
                    "path": "Tests/test_file_ppm.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "6a0a5a445",
                "new_commit_hash": "1bfd0434e",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/Tests/test_file_ppm.py"
            },
            "plus_file": {
                "path": "b/Tests/test_file_ppm.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 241,
                            "length": 13
                        },
                        "new_range": {
                            "start": 241,
                            "length": 23
                        },
                        "section": "def test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "            im.load()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "def test_plain_ppm_value_negative(tmp_path: Path) -> None:",
                                "type": "added"
                            },
                            {
                                "content": "    path = str(tmp_path / \"temp.ppm\")",
                                "type": "added"
                            },
                            {
                                "content": "    with open(path, \"wb\") as f:",
                                "type": "added"
                            },
                            {
                                "content": "        f.write(b\"P3\\n128 128\\n255\\n-1\")",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "    with Image.open(path) as im:",
                                "type": "added"
                            },
                            {
                                "content": "        with pytest.raises(ValueError, match=\"Channel value is negative\"):",
                                "type": "added"
                            },
                            {
                                "content": "            im.load()",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "",
                                "type": "added"
                            },
                            {
                                "content": "def test_plain_ppm_value_too_large(tmp_path: Path) -> None:",
                                "type": "context"
                            },
                            {
                                "content": "    path = str(tmp_path / \"temp.ppm\")",
                                "type": "context"
                            },
                            {
                                "content": "    with open(path, \"wb\") as f:",
                                "type": "context"
                            },
                            {
                                "content": "        f.write(b\"P3\\n128 128\\n255\\n256\")",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "    with Image.open(path) as im:",
                                "type": "context"
                            },
                            {
                                "content": "        with pytest.raises(ValueError):",
                                "type": "deleted"
                            },
                            {
                                "content": "        with pytest.raises(ValueError, match=\"Channel value too large\"):",
                                "type": "added"
                            },
                            {
                                "content": "            im.load()",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "Tests/test_file_ppm.py",
                            "type": "function",
                            "name": "test_plain_ppm_value_too_large",
                            "content": "def test_plain_ppm_value_too_large(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n256\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 244,
                            "end_lineno": 251,
                            "parent": null
                        }
                    ],
                    "added_entities": [
                        {
                            "file_name": "Tests/test_file_ppm.py",
                            "type": "function",
                            "name": "test_plain_ppm_value_negative",
                            "content": "def test_plain_ppm_value_negative(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n-1\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError, match=\"Channel value is negative\"):\n            im.load()",
                            "ast_type_str": "FunctionDef",
                            "start_lineno": 244,
                            "end_lineno": 251,
                            "parent": null
                        }
                    ],
                    "deleted_entities": []
                }
            ]
        },
        {
            "old_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# PPM support for PIL\n#\n# History:\n#       96-03-24 fl     Created\n#       98-03-06 fl     Write RGBA images (as RGB, that is)\n#\n# Copyright (c) Secret Labs AB 1997-98.\n# Copyright (c) Fredrik Lundh 1996.\n#\n# See the README file for information on usage and redistribution.\n#\nfrom __future__ import annotations\n\nimport math\nfrom typing import IO\n\nfrom . import Image, ImageFile\nfrom ._binary import i16be as i16\nfrom ._binary import o8\nfrom ._binary import o32le as o32\n\n#\n# --------------------------------------------------------------------\n\nb_whitespace = b\"\\x20\\x09\\x0a\\x0b\\x0c\\x0d\"\n\nMODES = {\n    # standard\n    b\"P1\": \"1\",\n    b\"P2\": \"L\",\n    b\"P3\": \"RGB\",\n    b\"P4\": \"1\",\n    b\"P5\": \"L\",\n    b\"P6\": \"RGB\",\n    # extensions\n    b\"P0CMYK\": \"CMYK\",\n    b\"Pf\": \"F\",\n    # PIL extensions (for test purposes only)\n    b\"PyP\": \"P\",\n    b\"PyRGBA\": \"RGBA\",\n    b\"PyCMYK\": \"CMYK\",\n}\n\n\ndef _accept(prefix: bytes) -> bool:\n    return prefix[0:1] == b\"P\" and prefix[1] in b\"0123456fy\"\n\n\n##\n# Image plugin for PBM, PGM, and PPM images.\n\n\nclass PpmImageFile(ImageFile.ImageFile):\n    format = \"PPM\"\n    format_description = \"Pbmplus image\"\n\n    def _read_magic(self) -> bytes:\n        assert self.fp is not None\n\n        magic = b\"\"\n        # read until whitespace or longest available magic number\n        for _ in range(6):\n            c = self.fp.read(1)\n            if not c or c in b_whitespace:\n                break\n            magic += c\n        return magic\n\n    def _read_token(self) -> bytes:\n        assert self.fp is not None\n\n        token = b\"\"\n        while len(token) <= 10:  # read until next whitespace or limit of 10 characters\n            c = self.fp.read(1)\n            if not c:\n                break\n            elif c in b_whitespace:  # token ended\n                if not token:\n                    # skip whitespace at start\n                    continue\n                break\n            elif c == b\"#\":\n                # ignores rest of the line; stops at CR, LF or EOF\n                while self.fp.read(1) not in b\"\\r\\n\":\n                    pass\n                continue\n            token += c\n        if not token:\n            # Token was not even 1 byte\n            msg = \"Reached EOF while reading header\"\n            raise ValueError(msg)\n        elif len(token) > 10:\n            msg = f\"Token too long in file header: {token.decode()}\"\n            raise ValueError(msg)\n        return token\n\n    def _open(self) -> None:\n        assert self.fp is not None\n\n        magic_number = self._read_magic()\n        try:\n            mode = MODES[magic_number]\n        except KeyError:\n            msg = \"not a PPM file\"\n            raise SyntaxError(msg)\n        self._mode = mode\n\n        if magic_number in (b\"P1\", b\"P4\"):\n            self.custom_mimetype = \"image/x-portable-bitmap\"\n        elif magic_number in (b\"P2\", b\"P5\"):\n            self.custom_mimetype = \"image/x-portable-graymap\"\n        elif magic_number in (b\"P3\", b\"P6\"):\n            self.custom_mimetype = \"image/x-portable-pixmap\"\n\n        self._size = int(self._read_token()), int(self._read_token())\n\n        decoder_name = \"raw\"\n        if magic_number in (b\"P1\", b\"P2\", b\"P3\"):\n            decoder_name = \"ppm_plain\"\n\n        args: str | tuple[str | int, ...]\n        if mode == \"1\":\n            args = \"1;I\"\n        elif mode == \"F\":\n            scale = float(self._read_token())\n            if scale == 0.0 or not math.isfinite(scale):\n                msg = \"scale must be finite and non-zero\"\n                raise ValueError(msg)\n            self.info[\"scale\"] = abs(scale)\n\n            rawmode = \"F;32F\" if scale < 0 else \"F;32BF\"\n            args = (rawmode, 0, -1)\n        else:\n            maxval = int(self._read_token())\n            if not 0 < maxval < 65536:\n                msg = \"maxval must be greater than 0 and less than 65536\"\n                raise ValueError(msg)\n            if maxval > 255 and mode == \"L\":\n                self._mode = \"I\"\n\n            rawmode = mode\n            if decoder_name != \"ppm_plain\":\n                # If maxval matches a bit depth, use the raw decoder directly\n                if maxval == 65535 and mode == \"L\":\n                    rawmode = \"I;16B\"\n                elif maxval != 255:\n                    decoder_name = \"ppm\"\n\n            args = rawmode if decoder_name == \"raw\" else (rawmode, maxval)\n        self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]\n\n\n#\n# --------------------------------------------------------------------\n\n\nclass PpmPlainDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n    _comment_spans: bool\n\n    def _read_block(self) -> bytes:\n        assert self.fd is not None\n\n        return self.fd.read(ImageFile.SAFEBLOCK)\n\n    def _find_comment_end(self, block: bytes, start: int = 0) -> int:\n        a = block.find(b\"\\n\", start)\n        b = block.find(b\"\\r\", start)\n        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)\n\n    def _ignore_comments(self, block: bytes) -> bytes:\n        if self._comment_spans:\n            # Finish current comment\n            while block:\n                comment_end = self._find_comment_end(block)\n                if comment_end != -1:\n                    # Comment ends in this block\n                    # Delete tail of comment\n                    block = block[comment_end + 1 :]\n                    break\n                else:\n                    # Comment spans whole block\n                    # So read the next block, looking for the end\n                    block = self._read_block()\n\n        # Search for any further comments\n        self._comment_spans = False\n        while True:\n            comment_start = block.find(b\"#\")\n            if comment_start == -1:\n                # No comment found\n                break\n            comment_end = self._find_comment_end(block, comment_start)\n            if comment_end != -1:\n                # Comment ends in this block\n                # Delete comment\n                block = block[:comment_start] + block[comment_end + 1 :]\n            else:\n                # Comment continues to next block(s)\n                block = block[:comment_start]\n                self._comment_spans = True\n                break\n        return block\n\n    def _decode_bitonal(self) -> bytearray:\n        \"\"\"\n        This is a separate method because in the plain PBM format, all data tokens are\n        exactly one byte, so the inter-token whitespace is optional.\n        \"\"\"\n        data = bytearray()\n        total_bytes = self.state.xsize * self.state.ysize\n\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                # eof\n                break\n\n            block = self._ignore_comments(block)\n\n            tokens = b\"\".join(block.split())\n            for token in tokens:\n                if token not in (48, 49):\n                    msg = b\"Invalid token for this mode: %s\" % bytes([token])\n                    raise ValueError(msg)\n            data = (data + tokens)[:total_bytes]\n        invert = bytes.maketrans(b\"01\", b\"\\xFF\\x00\")\n        return data.translate(invert)\n\n    def _decode_blocks(self, maxval: int) -> bytearray:\n        data = bytearray()\n        max_len = 10\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count\n\n        half_token = b\"\"\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                if half_token:\n                    block = bytearray(b\" \")  # flush half_token\n                else:\n                    # eof\n                    break\n\n            block = self._ignore_comments(block)\n\n            if half_token:\n                block = half_token + block  # stitch half_token to new block\n                half_token = b\"\"\n\n            tokens = block.split()\n\n            if block and not block[-1:].isspace():  # block might split token\n                half_token = tokens.pop()  # save half token for later\n                if len(half_token) > max_len:  # prevent buildup of half_token\n                    msg = (\n                        b\"Token too long found in data: %s\" % half_token[: max_len + 1]\n                    )\n                    raise ValueError(msg)\n\n            for token in tokens:\n                if len(token) > max_len:\n                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]\n                    raise ValueError(msg)\n                value = int(token)\n                if value > maxval:\n                    msg_str = f\"Channel value too large for this mode: {value}\"\n                    raise ValueError(msg_str)\n                value = round(value / maxval * out_max)\n                data += o32(value) if self.mode == \"I\" else o8(value)\n                if len(data) == total_bytes:  # finished!\n                    break\n        return data\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        self._comment_spans = False\n        if self.mode == \"1\":\n            data = self._decode_bitonal()\n            rawmode = \"1;8\"\n        else:\n            maxval = self.args[-1]\n            data = self._decode_blocks(maxval)\n            rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0\n\n\nclass PpmDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        assert self.fd is not None\n\n        data = bytearray()\n        maxval = self.args[-1]\n        in_byte_count = 1 if maxval < 256 else 2\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:\n            pixels = self.fd.read(in_byte_count * bands)\n            if len(pixels) < in_byte_count * bands:\n                # eof\n                break\n            for b in range(bands):\n                value = (\n                    pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)\n                )\n                value = min(out_max, round(value / maxval * out_max))\n                data += o32(value) if self.mode == \"I\" else o8(value)\n        rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0\n\n\n#\n# --------------------------------------------------------------------\n\n\ndef _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:\n    if im.mode == \"1\":\n        rawmode, head = \"1;I\", b\"P4\"\n    elif im.mode == \"L\":\n        rawmode, head = \"L\", b\"P5\"\n    elif im.mode == \"I\":\n        rawmode, head = \"I;16B\", b\"P5\"\n    elif im.mode in (\"RGB\", \"RGBA\"):\n        rawmode, head = \"RGB\", b\"P6\"\n    elif im.mode == \"F\":\n        rawmode, head = \"F;32F\", b\"Pf\"\n    else:\n        msg = f\"cannot write mode {im.mode} as PPM\"\n        raise OSError(msg)\n    fp.write(head + b\"\\n%d %d\\n\" % im.size)\n    if head == b\"P6\":\n        fp.write(b\"255\\n\")\n    elif head == b\"P5\":\n        if rawmode == \"L\":\n            fp.write(b\"255\\n\")\n        else:\n            fp.write(b\"65535\\n\")\n    elif head == b\"Pf\":\n        fp.write(b\"-1.0\\n\")\n    row_order = -1 if im.mode == \"F\" else 1\n    ImageFile._save(im, fp, [(\"raw\", (0, 0) + im.size, 0, (rawmode, 0, row_order))])\n\n\n#\n# --------------------------------------------------------------------\n\n\nImage.register_open(PpmImageFile.format, PpmImageFile, _accept)\nImage.register_save(PpmImageFile.format, _save)\n\nImage.register_decoder(\"ppm\", PpmDecoder)\nImage.register_decoder(\"ppm_plain\", PpmPlainDecoder)\n\nImage.register_extensions(PpmImageFile.format, [\".pbm\", \".pgm\", \".ppm\", \".pnm\", \".pfm\"])\n\nImage.register_mime(PpmImageFile.format, \"image/x-portable-anymap\")\n",
            "new_file_content": "#\n# The Python Imaging Library.\n# $Id$\n#\n# PPM support for PIL\n#\n# History:\n#       96-03-24 fl     Created\n#       98-03-06 fl     Write RGBA images (as RGB, that is)\n#\n# Copyright (c) Secret Labs AB 1997-98.\n# Copyright (c) Fredrik Lundh 1996.\n#\n# See the README file for information on usage and redistribution.\n#\nfrom __future__ import annotations\n\nimport math\nfrom typing import IO\n\nfrom . import Image, ImageFile\nfrom ._binary import i16be as i16\nfrom ._binary import o8\nfrom ._binary import o32le as o32\n\n#\n# --------------------------------------------------------------------\n\nb_whitespace = b\"\\x20\\x09\\x0a\\x0b\\x0c\\x0d\"\n\nMODES = {\n    # standard\n    b\"P1\": \"1\",\n    b\"P2\": \"L\",\n    b\"P3\": \"RGB\",\n    b\"P4\": \"1\",\n    b\"P5\": \"L\",\n    b\"P6\": \"RGB\",\n    # extensions\n    b\"P0CMYK\": \"CMYK\",\n    b\"Pf\": \"F\",\n    # PIL extensions (for test purposes only)\n    b\"PyP\": \"P\",\n    b\"PyRGBA\": \"RGBA\",\n    b\"PyCMYK\": \"CMYK\",\n}\n\n\ndef _accept(prefix: bytes) -> bool:\n    return prefix[0:1] == b\"P\" and prefix[1] in b\"0123456fy\"\n\n\n##\n# Image plugin for PBM, PGM, and PPM images.\n\n\nclass PpmImageFile(ImageFile.ImageFile):\n    format = \"PPM\"\n    format_description = \"Pbmplus image\"\n\n    def _read_magic(self) -> bytes:\n        assert self.fp is not None\n\n        magic = b\"\"\n        # read until whitespace or longest available magic number\n        for _ in range(6):\n            c = self.fp.read(1)\n            if not c or c in b_whitespace:\n                break\n            magic += c\n        return magic\n\n    def _read_token(self) -> bytes:\n        assert self.fp is not None\n\n        token = b\"\"\n        while len(token) <= 10:  # read until next whitespace or limit of 10 characters\n            c = self.fp.read(1)\n            if not c:\n                break\n            elif c in b_whitespace:  # token ended\n                if not token:\n                    # skip whitespace at start\n                    continue\n                break\n            elif c == b\"#\":\n                # ignores rest of the line; stops at CR, LF or EOF\n                while self.fp.read(1) not in b\"\\r\\n\":\n                    pass\n                continue\n            token += c\n        if not token:\n            # Token was not even 1 byte\n            msg = \"Reached EOF while reading header\"\n            raise ValueError(msg)\n        elif len(token) > 10:\n            msg = f\"Token too long in file header: {token.decode()}\"\n            raise ValueError(msg)\n        return token\n\n    def _open(self) -> None:\n        assert self.fp is not None\n\n        magic_number = self._read_magic()\n        try:\n            mode = MODES[magic_number]\n        except KeyError:\n            msg = \"not a PPM file\"\n            raise SyntaxError(msg)\n        self._mode = mode\n\n        if magic_number in (b\"P1\", b\"P4\"):\n            self.custom_mimetype = \"image/x-portable-bitmap\"\n        elif magic_number in (b\"P2\", b\"P5\"):\n            self.custom_mimetype = \"image/x-portable-graymap\"\n        elif magic_number in (b\"P3\", b\"P6\"):\n            self.custom_mimetype = \"image/x-portable-pixmap\"\n\n        self._size = int(self._read_token()), int(self._read_token())\n\n        decoder_name = \"raw\"\n        if magic_number in (b\"P1\", b\"P2\", b\"P3\"):\n            decoder_name = \"ppm_plain\"\n\n        args: str | tuple[str | int, ...]\n        if mode == \"1\":\n            args = \"1;I\"\n        elif mode == \"F\":\n            scale = float(self._read_token())\n            if scale == 0.0 or not math.isfinite(scale):\n                msg = \"scale must be finite and non-zero\"\n                raise ValueError(msg)\n            self.info[\"scale\"] = abs(scale)\n\n            rawmode = \"F;32F\" if scale < 0 else \"F;32BF\"\n            args = (rawmode, 0, -1)\n        else:\n            maxval = int(self._read_token())\n            if not 0 < maxval < 65536:\n                msg = \"maxval must be greater than 0 and less than 65536\"\n                raise ValueError(msg)\n            if maxval > 255 and mode == \"L\":\n                self._mode = \"I\"\n\n            rawmode = mode\n            if decoder_name != \"ppm_plain\":\n                # If maxval matches a bit depth, use the raw decoder directly\n                if maxval == 65535 and mode == \"L\":\n                    rawmode = \"I;16B\"\n                elif maxval != 255:\n                    decoder_name = \"ppm\"\n\n            args = rawmode if decoder_name == \"raw\" else (rawmode, maxval)\n        self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]\n\n\n#\n# --------------------------------------------------------------------\n\n\nclass PpmPlainDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n    _comment_spans: bool\n\n    def _read_block(self) -> bytes:\n        assert self.fd is not None\n\n        return self.fd.read(ImageFile.SAFEBLOCK)\n\n    def _find_comment_end(self, block: bytes, start: int = 0) -> int:\n        a = block.find(b\"\\n\", start)\n        b = block.find(b\"\\r\", start)\n        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)\n\n    def _ignore_comments(self, block: bytes) -> bytes:\n        if self._comment_spans:\n            # Finish current comment\n            while block:\n                comment_end = self._find_comment_end(block)\n                if comment_end != -1:\n                    # Comment ends in this block\n                    # Delete tail of comment\n                    block = block[comment_end + 1 :]\n                    break\n                else:\n                    # Comment spans whole block\n                    # So read the next block, looking for the end\n                    block = self._read_block()\n\n        # Search for any further comments\n        self._comment_spans = False\n        while True:\n            comment_start = block.find(b\"#\")\n            if comment_start == -1:\n                # No comment found\n                break\n            comment_end = self._find_comment_end(block, comment_start)\n            if comment_end != -1:\n                # Comment ends in this block\n                # Delete comment\n                block = block[:comment_start] + block[comment_end + 1 :]\n            else:\n                # Comment continues to next block(s)\n                block = block[:comment_start]\n                self._comment_spans = True\n                break\n        return block\n\n    def _decode_bitonal(self) -> bytearray:\n        \"\"\"\n        This is a separate method because in the plain PBM format, all data tokens are\n        exactly one byte, so the inter-token whitespace is optional.\n        \"\"\"\n        data = bytearray()\n        total_bytes = self.state.xsize * self.state.ysize\n\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                # eof\n                break\n\n            block = self._ignore_comments(block)\n\n            tokens = b\"\".join(block.split())\n            for token in tokens:\n                if token not in (48, 49):\n                    msg = b\"Invalid token for this mode: %s\" % bytes([token])\n                    raise ValueError(msg)\n            data = (data + tokens)[:total_bytes]\n        invert = bytes.maketrans(b\"01\", b\"\\xFF\\x00\")\n        return data.translate(invert)\n\n    def _decode_blocks(self, maxval: int) -> bytearray:\n        data = bytearray()\n        max_len = 10\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count\n\n        half_token = b\"\"\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                if half_token:\n                    block = bytearray(b\" \")  # flush half_token\n                else:\n                    # eof\n                    break\n\n            block = self._ignore_comments(block)\n\n            if half_token:\n                block = half_token + block  # stitch half_token to new block\n                half_token = b\"\"\n\n            tokens = block.split()\n\n            if block and not block[-1:].isspace():  # block might split token\n                half_token = tokens.pop()  # save half token for later\n                if len(half_token) > max_len:  # prevent buildup of half_token\n                    msg = (\n                        b\"Token too long found in data: %s\" % half_token[: max_len + 1]\n                    )\n                    raise ValueError(msg)\n\n            for token in tokens:\n                if len(token) > max_len:\n                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]\n                    raise ValueError(msg)\n                value = int(token)\n                if value < 0:\n                    msg_str = f\"Channel value is negative: {value}\"\n                    raise ValueError(msg_str)\n                if value > maxval:\n                    msg_str = f\"Channel value too large for this mode: {value}\"\n                    raise ValueError(msg_str)\n                value = round(value / maxval * out_max)\n                data += o32(value) if self.mode == \"I\" else o8(value)\n                if len(data) == total_bytes:  # finished!\n                    break\n        return data\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        self._comment_spans = False\n        if self.mode == \"1\":\n            data = self._decode_bitonal()\n            rawmode = \"1;8\"\n        else:\n            maxval = self.args[-1]\n            data = self._decode_blocks(maxval)\n            rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0\n\n\nclass PpmDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        assert self.fd is not None\n\n        data = bytearray()\n        maxval = self.args[-1]\n        in_byte_count = 1 if maxval < 256 else 2\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:\n            pixels = self.fd.read(in_byte_count * bands)\n            if len(pixels) < in_byte_count * bands:\n                # eof\n                break\n            for b in range(bands):\n                value = (\n                    pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)\n                )\n                value = min(out_max, round(value / maxval * out_max))\n                data += o32(value) if self.mode == \"I\" else o8(value)\n        rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0\n\n\n#\n# --------------------------------------------------------------------\n\n\ndef _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:\n    if im.mode == \"1\":\n        rawmode, head = \"1;I\", b\"P4\"\n    elif im.mode == \"L\":\n        rawmode, head = \"L\", b\"P5\"\n    elif im.mode == \"I\":\n        rawmode, head = \"I;16B\", b\"P5\"\n    elif im.mode in (\"RGB\", \"RGBA\"):\n        rawmode, head = \"RGB\", b\"P6\"\n    elif im.mode == \"F\":\n        rawmode, head = \"F;32F\", b\"Pf\"\n    else:\n        msg = f\"cannot write mode {im.mode} as PPM\"\n        raise OSError(msg)\n    fp.write(head + b\"\\n%d %d\\n\" % im.size)\n    if head == b\"P6\":\n        fp.write(b\"255\\n\")\n    elif head == b\"P5\":\n        if rawmode == \"L\":\n            fp.write(b\"255\\n\")\n        else:\n            fp.write(b\"65535\\n\")\n    elif head == b\"Pf\":\n        fp.write(b\"-1.0\\n\")\n    row_order = -1 if im.mode == \"F\" else 1\n    ImageFile._save(im, fp, [(\"raw\", (0, 0) + im.size, 0, (rawmode, 0, row_order))])\n\n\n#\n# --------------------------------------------------------------------\n\n\nImage.register_open(PpmImageFile.format, PpmImageFile, _accept)\nImage.register_save(PpmImageFile.format, _save)\n\nImage.register_decoder(\"ppm\", PpmDecoder)\nImage.register_decoder(\"ppm_plain\", PpmPlainDecoder)\n\nImage.register_extensions(PpmImageFile.format, [\".pbm\", \".pgm\", \".ppm\", \".pnm\", \".pfm\"])\n\nImage.register_mime(PpmImageFile.format, \"image/x-portable-anymap\")\n",
            "header": {
                "file": {
                    "path": "src/PIL/PpmImagePlugin.py"
                },
                "misc_line": null
            },
            "index_line": {
                "old_commit_hash": "6ac7a9bbc",
                "new_commit_hash": "bca3018c3",
                "mode": "100644"
            },
            "is_binary_file": false,
            "binary_line": null,
            "minus_file": {
                "path": "a/src/PIL/PpmImagePlugin.py"
            },
            "plus_file": {
                "path": "b/src/PIL/PpmImagePlugin.py"
            },
            "hunks": [
                {
                    "descriptor": {
                        "old_range": {
                            "start": 270,
                            "length": 6
                        },
                        "new_range": {
                            "start": 270,
                            "length": 9
                        },
                        "section": "class PpmPlainDecoder(ImageFile.PyDecoder):"
                    },
                    "line_group": {
                        "all_lines": [
                            {
                                "content": "                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]",
                                "type": "context"
                            },
                            {
                                "content": "                    raise ValueError(msg)",
                                "type": "context"
                            },
                            {
                                "content": "                value = int(token)",
                                "type": "context"
                            },
                            {
                                "content": "                if value < 0:",
                                "type": "added"
                            },
                            {
                                "content": "                    msg_str = f\"Channel value is negative: {value}\"",
                                "type": "added"
                            },
                            {
                                "content": "                    raise ValueError(msg_str)",
                                "type": "added"
                            },
                            {
                                "content": "                if value > maxval:",
                                "type": "context"
                            },
                            {
                                "content": "                    msg_str = f\"Channel value too large for this mode: {value}\"",
                                "type": "context"
                            },
                            {
                                "content": "                    raise ValueError(msg_str)",
                                "type": "context"
                            },
                            {
                                "content": "",
                                "type": "context"
                            }
                        ]
                    },
                    "modified_entities": [
                        {
                            "file_name": "src/PIL/PpmImagePlugin.py",
                            "type": "class",
                            "name": "PpmPlainDecoder",
                            "content": "class PpmPlainDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n    _comment_spans: bool\n\n    def _read_block(self) -> bytes:\n        assert self.fd is not None\n\n        return self.fd.read(ImageFile.SAFEBLOCK)\n\n    def _find_comment_end(self, block: bytes, start: int = 0) -> int:\n        a = block.find(b\"\\n\", start)\n        b = block.find(b\"\\r\", start)\n        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)\n\n    def _ignore_comments(self, block: bytes) -> bytes:\n        if self._comment_spans:\n            # Finish current comment\n            while block:\n                comment_end = self._find_comment_end(block)\n                if comment_end != -1:\n                    # Comment ends in this block\n                    # Delete tail of comment\n                    block = block[comment_end + 1 :]\n                    break\n                else:\n                    # Comment spans whole block\n                    # So read the next block, looking for the end\n                    block = self._read_block()\n\n        # Search for any further comments\n        self._comment_spans = False\n        while True:\n            comment_start = block.find(b\"#\")\n            if comment_start == -1:\n                # No comment found\n                break\n            comment_end = self._find_comment_end(block, comment_start)\n            if comment_end != -1:\n                # Comment ends in this block\n                # Delete comment\n                block = block[:comment_start] + block[comment_end + 1 :]\n            else:\n                # Comment continues to next block(s)\n                block = block[:comment_start]\n                self._comment_spans = True\n                break\n        return block\n\n    def _decode_bitonal(self) -> bytearray:\n        \"\"\"\n        This is a separate method because in the plain PBM format, all data tokens are\n        exactly one byte, so the inter-token whitespace is optional.\n        \"\"\"\n        data = bytearray()\n        total_bytes = self.state.xsize * self.state.ysize\n\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                # eof\n                break\n\n            block = self._ignore_comments(block)\n\n            tokens = b\"\".join(block.split())\n            for token in tokens:\n                if token not in (48, 49):\n                    msg = b\"Invalid token for this mode: %s\" % bytes([token])\n                    raise ValueError(msg)\n            data = (data + tokens)[:total_bytes]\n        invert = bytes.maketrans(b\"01\", b\"\\xFF\\x00\")\n        return data.translate(invert)\n\n    def _decode_blocks(self, maxval: int) -> bytearray:\n        data = bytearray()\n        max_len = 10\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count\n\n        half_token = b\"\"\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                if half_token:\n                    block = bytearray(b\" \")  # flush half_token\n                else:\n                    # eof\n                    break\n\n            block = self._ignore_comments(block)\n\n            if half_token:\n                block = half_token + block  # stitch half_token to new block\n                half_token = b\"\"\n\n            tokens = block.split()\n\n            if block and not block[-1:].isspace():  # block might split token\n                half_token = tokens.pop()  # save half token for later\n                if len(half_token) > max_len:  # prevent buildup of half_token\n                    msg = (\n                        b\"Token too long found in data: %s\" % half_token[: max_len + 1]\n                    )\n                    raise ValueError(msg)\n\n            for token in tokens:\n                if len(token) > max_len:\n                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]\n                    raise ValueError(msg)\n                value = int(token)\n                if value < 0:\n                    msg_str = f\"Channel value is negative: {value}\"\n                    raise ValueError(msg_str)\n                if value > maxval:\n                    msg_str = f\"Channel value too large for this mode: {value}\"\n                    raise ValueError(msg_str)\n                value = round(value / maxval * out_max)\n                data += o32(value) if self.mode == \"I\" else o8(value)\n                if len(data) == total_bytes:  # finished!\n                    break\n        return data\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        self._comment_spans = False\n        if self.mode == \"1\":\n            data = self._decode_bitonal()\n            rawmode = \"1;8\"\n        else:\n            maxval = self.args[-1]\n            data = self._decode_blocks(maxval)\n            rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 161,
                            "end_lineno": 295,
                            "parent": null
                        },
                        {
                            "file_name": "src/PIL/PpmImagePlugin.py",
                            "type": "method",
                            "name": "PpmPlainDecoder._decode_blocks",
                            "content": "class PpmPlainDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n    _comment_spans: bool\n\n    def _read_block(self) -> bytes:\n        assert self.fd is not None\n\n        return self.fd.read(ImageFile.SAFEBLOCK)\n\n    def _find_comment_end(self, block: bytes, start: int = 0) -> int:\n        a = block.find(b\"\\n\", start)\n        b = block.find(b\"\\r\", start)\n        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)\n\n    def _ignore_comments(self, block: bytes) -> bytes:\n        if self._comment_spans:\n            # Finish current comment\n            while block:\n                comment_end = self._find_comment_end(block)\n                if comment_end != -1:\n                    # Comment ends in this block\n                    # Delete tail of comment\n                    block = block[comment_end + 1 :]\n                    break\n                else:\n                    # Comment spans whole block\n                    # So read the next block, looking for the end\n                    block = self._read_block()\n\n        # Search for any further comments\n        self._comment_spans = False\n        while True:\n            comment_start = block.find(b\"#\")\n            if comment_start == -1:\n                # No comment found\n                break\n            comment_end = self._find_comment_end(block, comment_start)\n            if comment_end != -1:\n                # Comment ends in this block\n                # Delete comment\n                block = block[:comment_start] + block[comment_end + 1 :]\n            else:\n                # Comment continues to next block(s)\n                block = block[:comment_start]\n                self._comment_spans = True\n                break\n        return block\n\n    def _decode_bitonal(self) -> bytearray:\n        \"\"\"\n        This is a separate method because in the plain PBM format, all data tokens are\n        exactly one byte, so the inter-token whitespace is optional.\n        \"\"\"\n        data = bytearray()\n        total_bytes = self.state.xsize * self.state.ysize\n\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                # eof\n                break\n\n            block = self._ignore_comments(block)\n\n            tokens = b\"\".join(block.split())\n            for token in tokens:\n                if token not in (48, 49):\n                    msg = b\"Invalid token for this mode: %s\" % bytes([token])\n                    raise ValueError(msg)\n            data = (data + tokens)[:total_bytes]\n        invert = bytes.maketrans(b\"01\", b\"\\xFF\\x00\")\n        return data.translate(invert)\n\n    def _decode_blocks(self, maxval: int) -> bytearray:\n        data = bytearray()\n        max_len = 10\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count\n\n        half_token = b\"\"\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                if half_token:\n                    block = bytearray(b\" \")  # flush half_token\n                else:\n                    # eof\n                    break\n\n            block = self._ignore_comments(block)\n\n            if half_token:\n                block = half_token + block  # stitch half_token to new block\n                half_token = b\"\"\n\n            tokens = block.split()\n\n            if block and not block[-1:].isspace():  # block might split token\n                half_token = tokens.pop()  # save half token for later\n                if len(half_token) > max_len:  # prevent buildup of half_token\n                    msg = (\n                        b\"Token too long found in data: %s\" % half_token[: max_len + 1]\n                    )\n                    raise ValueError(msg)\n\n            for token in tokens:\n                if len(token) > max_len:\n                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]\n                    raise ValueError(msg)\n                value = int(token)\n                if value < 0:\n                    msg_str = f\"Channel value is negative: {value}\"\n                    raise ValueError(msg_str)\n                if value > maxval:\n                    msg_str = f\"Channel value too large for this mode: {value}\"\n                    raise ValueError(msg_str)\n                value = round(value / maxval * out_max)\n                data += o32(value) if self.mode == \"I\" else o8(value)\n                if len(data) == total_bytes:  # finished!\n                    break\n        return data\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        self._comment_spans = False\n        if self.mode == \"1\":\n            data = self._decode_bitonal()\n            rawmode = \"1;8\"\n        else:\n            maxval = self.args[-1]\n            data = self._decode_blocks(maxval)\n            rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0",
                            "ast_type_str": "ClassDef",
                            "start_lineno": 234,
                            "end_lineno": 283,
                            "parent": {
                                "file_name": "src/PIL/PpmImagePlugin.py",
                                "type": "class",
                                "name": "PpmPlainDecoder",
                                "content": "class PpmPlainDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n    _comment_spans: bool\n\n    def _read_block(self) -> bytes:\n        assert self.fd is not None\n\n        return self.fd.read(ImageFile.SAFEBLOCK)\n\n    def _find_comment_end(self, block: bytes, start: int = 0) -> int:\n        a = block.find(b\"\\n\", start)\n        b = block.find(b\"\\r\", start)\n        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)\n\n    def _ignore_comments(self, block: bytes) -> bytes:\n        if self._comment_spans:\n            # Finish current comment\n            while block:\n                comment_end = self._find_comment_end(block)\n                if comment_end != -1:\n                    # Comment ends in this block\n                    # Delete tail of comment\n                    block = block[comment_end + 1 :]\n                    break\n                else:\n                    # Comment spans whole block\n                    # So read the next block, looking for the end\n                    block = self._read_block()\n\n        # Search for any further comments\n        self._comment_spans = False\n        while True:\n            comment_start = block.find(b\"#\")\n            if comment_start == -1:\n                # No comment found\n                break\n            comment_end = self._find_comment_end(block, comment_start)\n            if comment_end != -1:\n                # Comment ends in this block\n                # Delete comment\n                block = block[:comment_start] + block[comment_end + 1 :]\n            else:\n                # Comment continues to next block(s)\n                block = block[:comment_start]\n                self._comment_spans = True\n                break\n        return block\n\n    def _decode_bitonal(self) -> bytearray:\n        \"\"\"\n        This is a separate method because in the plain PBM format, all data tokens are\n        exactly one byte, so the inter-token whitespace is optional.\n        \"\"\"\n        data = bytearray()\n        total_bytes = self.state.xsize * self.state.ysize\n\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                # eof\n                break\n\n            block = self._ignore_comments(block)\n\n            tokens = b\"\".join(block.split())\n            for token in tokens:\n                if token not in (48, 49):\n                    msg = b\"Invalid token for this mode: %s\" % bytes([token])\n                    raise ValueError(msg)\n            data = (data + tokens)[:total_bytes]\n        invert = bytes.maketrans(b\"01\", b\"\\xFF\\x00\")\n        return data.translate(invert)\n\n    def _decode_blocks(self, maxval: int) -> bytearray:\n        data = bytearray()\n        max_len = 10\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count\n\n        half_token = b\"\"\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                if half_token:\n                    block = bytearray(b\" \")  # flush half_token\n                else:\n                    # eof\n                    break\n\n            block = self._ignore_comments(block)\n\n            if half_token:\n                block = half_token + block  # stitch half_token to new block\n                half_token = b\"\"\n\n            tokens = block.split()\n\n            if block and not block[-1:].isspace():  # block might split token\n                half_token = tokens.pop()  # save half token for later\n                if len(half_token) > max_len:  # prevent buildup of half_token\n                    msg = (\n                        b\"Token too long found in data: %s\" % half_token[: max_len + 1]\n                    )\n                    raise ValueError(msg)\n\n            for token in tokens:\n                if len(token) > max_len:\n                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]\n                    raise ValueError(msg)\n                value = int(token)\n                if value < 0:\n                    msg_str = f\"Channel value is negative: {value}\"\n                    raise ValueError(msg_str)\n                if value > maxval:\n                    msg_str = f\"Channel value too large for this mode: {value}\"\n                    raise ValueError(msg_str)\n                value = round(value / maxval * out_max)\n                data += o32(value) if self.mode == \"I\" else o8(value)\n                if len(data) == total_bytes:  # finished!\n                    break\n        return data\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        self._comment_spans = False\n        if self.mode == \"1\":\n            data = self._decode_bitonal()\n            rawmode = \"1;8\"\n        else:\n            maxval = self.args[-1]\n            data = self._decode_blocks(maxval)\n            rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0",
                                "ast_type_str": "ClassDef",
                                "start_lineno": 161,
                                "end_lineno": 295,
                                "parent": null
                            }
                        }
                    ],
                    "added_entities": [],
                    "deleted_entities": []
                }
            ]
        }
    ],
    "old_commit_hash": "3ba8eb3b7e0cf6d02c0d62b315cc2db7afe969e8^",
    "new_commit_hash": "3ba8eb3b7e0cf6d02c0d62b315cc2db7afe969e8",
    "commit_message": "Raise a ValueError for negative values when loading P1-P3 PPM images",
    "commit_date": "2024-03-16T12:29:21+11:00",
    "metadata": {}
}
{
    "repo_name": "pillow",
    "new_commit_hash": "3ba8eb3b7e0cf6d02c0d62b315cc2db7afe969e8",
    "test_file_codes": [
        "from __future__ import annotations\n\nimport sys\nfrom io import BytesIO\nfrom pathlib import Path\n\nimport pytest\n\nfrom PIL import Image, PpmImagePlugin\n\nfrom .helper import (\n    assert_image_equal,\n    assert_image_equal_tofile,\n    assert_image_similar,\n    hopper,\n)\n\n# sample ppm stream\nTEST_FILE = \"Tests/images/hopper.ppm\"\n\n\ndef test_sanity() -> None:\n    with Image.open(TEST_FILE) as im:\n        assert im.mode == \"RGB\"\n        assert im.size == (128, 128)\n        assert im.format == \"PPM\"\n        assert im.get_format_mimetype() == \"image/x-portable-pixmap\"\n\n\n@pytest.mark.parametrize(\n    \"data, mode, pixels\",\n    (\n        (b\"P2 3 1 4 0 2 4\", \"L\", (0, 128, 255)),\n        (b\"P2 3 1 257 0 128 257\", \"I\", (0, 32640, 65535)),\n        # P3 with maxval < 255\n        (\n            b\"P3 3 1 17 0 1 2 8 9 10 15 16 17\",\n            \"RGB\",\n            ((0, 15, 30), (120, 135, 150), (225, 240, 255)),\n        ),\n        # P3 with maxval > 255\n        # Scale down to 255, since there is no RGB mode with more than 8-bit\n        (\n            b\"P3 3 1 257 0 1 2 128 129 130 256 257 257\",\n            \"RGB\",\n            ((0, 1, 2), (127, 128, 129), (254, 255, 255)),\n        ),\n        (b\"P5 3 1 4 \\x00\\x02\\x04\", \"L\", (0, 128, 255)),\n        (b\"P5 3 1 257 \\x00\\x00\\x00\\x80\\x01\\x01\", \"I\", (0, 32640, 65535)),\n        # P6 with maxval < 255\n        (\n            b\"P6 3 1 17 \\x00\\x01\\x02\\x08\\x09\\x0A\\x0F\\x10\\x11\",\n            \"RGB\",\n            (\n                (0, 15, 30),\n                (120, 135, 150),\n                (225, 240, 255),\n            ),\n        ),\n        # P6 with maxval > 255\n        (\n            b\"P6 3 1 257 \\x00\\x00\\x00\\x01\\x00\\x02\"\n            b\"\\x00\\x80\\x00\\x81\\x00\\x82\\x01\\x00\\x01\\x01\\xFF\\xFF\",\n            \"RGB\",\n            (\n                (0, 1, 2),\n                (127, 128, 129),\n                (254, 255, 255),\n            ),\n        ),\n    ),\n)\ndef test_arbitrary_maxval(\n    data: bytes, mode: str, pixels: tuple[int | tuple[int, int, int], ...]\n) -> None:\n    fp = BytesIO(data)\n    with Image.open(fp) as im:\n        assert im.size == (3, 1)\n        assert im.mode == mode\n\n        px = im.load()\n        assert tuple(px[x, 0] for x in range(3)) == pixels\n\n\ndef test_16bit_pgm() -> None:\n    with Image.open(\"Tests/images/16_bit_binary.pgm\") as im:\n        assert im.mode == \"I\"\n        assert im.size == (20, 100)\n        assert im.get_format_mimetype() == \"image/x-portable-graymap\"\n\n        assert_image_equal_tofile(im, \"Tests/images/16_bit_binary_pgm.tiff\")\n\n\ndef test_16bit_pgm_write(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/16_bit_binary.pgm\") as im:\n        filename = str(tmp_path / \"temp.pgm\")\n        im.save(filename, \"PPM\")\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pnm(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper.pnm\") as im:\n        assert_image_similar(im, hopper(), 0.0001)\n\n        filename = str(tmp_path / \"temp.pnm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pfm(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper.pfm\") as im:\n        assert im.info[\"scale\"] == 1.0\n        assert_image_equal(im, hopper(\"F\"))\n\n        filename = str(tmp_path / \"tmp.pfm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\ndef test_pfm_big_endian(tmp_path: Path) -> None:\n    with Image.open(\"Tests/images/hopper_be.pfm\") as im:\n        assert im.info[\"scale\"] == 2.5\n        assert_image_equal(im, hopper(\"F\"))\n\n        filename = str(tmp_path / \"tmp.pfm\")\n        im.save(filename)\n\n        assert_image_equal_tofile(im, filename)\n\n\n@pytest.mark.parametrize(\n    \"data\",\n    [\n        b\"Pf 1 1 NaN \\0\\0\\0\\0\",\n        b\"Pf 1 1 inf \\0\\0\\0\\0\",\n        b\"Pf 1 1 -inf \\0\\0\\0\\0\",\n        b\"Pf 1 1 0.0 \\0\\0\\0\\0\",\n        b\"Pf 1 1 -0.0 \\0\\0\\0\\0\",\n    ],\n)\ndef test_pfm_invalid(data: bytes) -> None:\n    with pytest.raises(ValueError):\n        with Image.open(BytesIO(data)):\n            pass\n\n\n@pytest.mark.parametrize(\n    \"plain_path, raw_path\",\n    (\n        (\n            \"Tests/images/hopper_1bit_plain.pbm\",  # P1\n            \"Tests/images/hopper_1bit.pbm\",  # P4\n        ),\n        (\n            \"Tests/images/hopper_8bit_plain.pgm\",  # P2\n            \"Tests/images/hopper_8bit.pgm\",  # P5\n        ),\n        (\n            \"Tests/images/hopper_8bit_plain.ppm\",  # P3\n            \"Tests/images/hopper_8bit.ppm\",  # P6\n        ),\n    ),\n)\ndef test_plain(plain_path: str, raw_path: str) -> None:\n    with Image.open(plain_path) as im:\n        assert_image_equal_tofile(im, raw_path)\n\n\ndef test_16bit_plain_pgm() -> None:\n    # P2 with maxval 2 ** 16 - 1\n    with Image.open(\"Tests/images/hopper_16bit_plain.pgm\") as im:\n        assert im.mode == \"I\"\n        assert im.size == (128, 128)\n        assert im.get_format_mimetype() == \"image/x-portable-graymap\"\n\n        # P5 with maxval 2 ** 16 - 1\n        assert_image_equal_tofile(im, \"Tests/images/hopper_16bit.pgm\")\n\n\n@pytest.mark.parametrize(\n    \"header, data, comment_count\",\n    (\n        (b\"P1\\n2 2\", b\"1010\", 10**6),\n        (b\"P2\\n3 1\\n4\", b\"0 2 4\", 1),\n        (b\"P3\\n2 2\\n255\", b\"0 0 0 001 1 1 2 2 2 255 255 255\", 10**6),\n    ),\n)\ndef test_plain_data_with_comment(\n    tmp_path: Path, header: bytes, data: bytes, comment_count: int\n) -> None:\n    path1 = str(tmp_path / \"temp1.ppm\")\n    path2 = str(tmp_path / \"temp2.ppm\")\n    comment = b\"# comment\" * comment_count\n    with open(path1, \"wb\") as f1, open(path2, \"wb\") as f2:\n        f1.write(header + b\"\\n\\n\" + data)\n        f2.write(header + b\"\\n\" + comment + b\"\\n\" + data + comment)\n\n    with Image.open(path1) as im:\n        assert_image_equal_tofile(im, path2)\n\n\n@pytest.mark.parametrize(\"data\", (b\"P1\\n128 128\\n\", b\"P3\\n128 128\\n255\\n\"))\ndef test_plain_truncated_data(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\"data\", (b\"P1\\n128 128\\n1009\", b\"P3\\n128 128\\n255\\n100A\"))\ndef test_plain_invalid_data(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\n    \"data\",\n    (\n        b\"P3\\n128 128\\n255\\n012345678910\",  # half token too long\n        b\"P3\\n128 128\\n255\\n012345678910 0\",  # token too long\n    ),\n)\ndef test_plain_ppm_token_too_long(tmp_path: Path, data: bytes) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(data)\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_plain_ppm_value_negative(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n-1\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError, match=\"Channel value is negative\"):\n            im.load()\n\n\ndef test_plain_ppm_value_too_large(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P3\\n128 128\\n255\\n256\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError, match=\"Channel value too large\"):\n            im.load()\n\n\ndef test_magic() -> None:\n    with pytest.raises(SyntaxError):\n        PpmImagePlugin.PpmImageFile(fp=BytesIO(b\"PyInvalid\"))\n\n\ndef test_header_with_comments(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6 #comment\\n#comment\\r12#comment\\r8\\n128 #comment\\n255\\n\")\n\n    with Image.open(path) as im:\n        assert im.size == (128, 128)\n\n\ndef test_non_integer_token(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\nTEST\")\n\n    with pytest.raises(ValueError):\n        with Image.open(path):\n            pass\n\n\ndef test_header_token_too_long(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\n 01234567890\")\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"Token too long in file header: 01234567890\"\n\n\ndef test_truncated_file(tmp_path: Path) -> None:\n    # Test EOF in header\n    path = str(tmp_path / \"temp.pgm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\")\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"Reached EOF while reading header\"\n\n    # Test EOF for PyDecoder\n    fp = BytesIO(b\"P5 3 1 4\")\n    with Image.open(fp) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\ndef test_not_enough_image_data(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P2 1 2 255 255\")\n\n    with Image.open(path) as im:\n        with pytest.raises(ValueError):\n            im.load()\n\n\n@pytest.mark.parametrize(\"maxval\", (b\"0\", b\"65536\"))\ndef test_invalid_maxval(maxval: bytes, tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.ppm\")\n    with open(path, \"wb\") as f:\n        f.write(b\"P6\\n3 1 \" + maxval)\n\n    with pytest.raises(ValueError) as e:\n        with Image.open(path):\n            pass\n\n    assert str(e.value) == \"maxval must be greater than 0 and less than 65536\"\n\n\ndef test_neg_ppm() -> None:\n    # Storage.c accepted negative values for xsize, ysize.  the\n    # internal open_ppm function didn't check for sanity but it\n    # has been removed. The default opener doesn't accept negative\n    # sizes.\n\n    with pytest.raises(OSError):\n        with Image.open(\"Tests/images/negative_size.ppm\"):\n            pass\n\n\ndef test_mimetypes(tmp_path: Path) -> None:\n    path = str(tmp_path / \"temp.pgm\")\n\n    with open(path, \"wb\") as f:\n        f.write(b\"P4\\n128 128\\n255\")\n    with Image.open(path) as im:\n        assert im.get_format_mimetype() == \"image/x-portable-bitmap\"\n\n    with open(path, \"wb\") as f:\n        f.write(b\"PyCMYK\\n128 128\\n255\")\n    with Image.open(path) as im:\n        assert im.get_format_mimetype() == \"image/x-portable-anymap\"\n\n\n@pytest.mark.parametrize(\"buffer\", (True, False))\ndef test_save_stdout(buffer: bool) -> None:\n    old_stdout = sys.stdout\n\n    if buffer:\n\n        class MyStdOut:\n            buffer = BytesIO()\n\n        mystdout = MyStdOut()\n    else:\n        mystdout = BytesIO()\n\n    sys.stdout = mystdout\n\n    with Image.open(TEST_FILE) as im:\n        im.save(sys.stdout, \"PPM\")\n\n    # Reset stdout\n    sys.stdout = old_stdout\n\n    if buffer:\n        mystdout = mystdout.buffer\n    with Image.open(mystdout) as reloaded:\n        assert_image_equal_tofile(reloaded, TEST_FILE)\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": "warning: `VIRTUAL_ENV=/home/gcpuser/r2e-edits-internal/.venv` does not match the project environment path `.venv` and will be ignored\nUsing CPython 3.9.21\nCreating virtual environment at: .venv\nActivate with: source .venv/bin/activate\nResolved 12 packages in 21ms\nInstalled 12 packages in 150ms\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 1.16s\nPrepared 1 package in 34.16s\nInstalled 1 package in 1ms\n + pillow==10.3.0.dev0 (from file:///home/gcpuser/buckets/local_repoeval_bucket/repos/pillow_3ba8eb3b7e0cf6d02c0d62b315cc2db7afe969e8)\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_3ba8eb3b7e0cf6d02c0d62b315cc2db7afe969e8\nconfigfile: pyproject.toml\nplugins: cov-6.0.0\ncollected 46 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.\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_sanity\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P2 3 1 4 0 2 4-L-pixels0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P2 3 1 257 0 128 257-I-pixels1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P3 3 1 17 0 1 2 8 9 10 15 16 17-RGB-pixels2]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P3 3 1 257 0 1 2 128 129 130 256 257 257-RGB-pixels3]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P5 3 1 4 \\x00\\x02\\x04-L-pixels4]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P5 3 1 257 \\x00\\x00\\x00\\x80\\x01\\x01-I-pixels5]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P6 3 1 17 \\x00\\x01\\x02\\x08\\t\\n\\x0f\\x10\\x11-RGB-pixels6]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P6 3 1 257 \\x00\\x00\\x00\\x01\\x00\\x02\\x00\\x80\\x00\\x81\\x00\\x82\\x01\\x00\\x01\\x01\\xff\\xff-RGB-pixels7]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_16bit_pgm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_16bit_pgm_write\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pnm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_big_endian\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 NaN \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 inf \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 -inf \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 0.0 \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 -0.0 \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain[Tests/images/hopper_1bit_plain.pbm-Tests/images/hopper_1bit.pbm]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain[Tests/images/hopper_8bit_plain.pgm-Tests/images/hopper_8bit.pgm]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain[Tests/images/hopper_8bit_plain.ppm-Tests/images/hopper_8bit.ppm]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_16bit_plain_pgm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_data_with_comment[P1\\n2 2-1010-1000000]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_data_with_comment[P2\\n3 1\\n4-0 2 4-1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_data_with_comment[P3\\n2 2\\n255-0 0 0 001 1 1 2 2 2 255 255 255-1000000]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_truncated_data[P1\\n128 128\\n]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_truncated_data[P3\\n128 128\\n255\\n]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_invalid_data[P1\\n128 128\\n1009]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_invalid_data[P3\\n128 128\\n255\\n100A]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_token_too_long[P3\\n128 128\\n255\\n012345678910]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_token_too_long[P3\\n128 128\\n255\\n012345678910 0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_value_negative\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_value_too_large\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_magic\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_header_with_comments\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_non_integer_token\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_header_token_too_long\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_truncated_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_not_enough_image_data\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_maxval[0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_maxval[65536]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_neg_ppm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_mimetypes\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_stdout[True]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_stdout[False]\u001b[0m\n\u001b[32m============================== \u001b[32m\u001b[1m46 passed\u001b[0m\u001b[32m in 0.49s\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_3ba8eb3b7e0cf6d02c0d62b315cc2db7afe969e8\nconfigfile: pyproject.toml\nplugins: cov-6.0.0\ncollected 46 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.\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[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[31m       [100%]\u001b[0m\n\n=================================== FAILURES ===================================\n\u001b[31m\u001b[1m________________________ test_plain_ppm_value_negative _________________________\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-187/test_plain_ppm_value_negative0')\n\n    def test_plain_ppm_value_negative(tmp_path: Path) -> None:\n        path = str(tmp_path / \"temp.ppm\")\n        with open(path, \"wb\") as f:\n            f.write(b\"P3\\n128 128\\n255\\n-1\")\n    \n        with Image.open(path) as im:\n            with pytest.raises(ValueError, match=\"Channel value is negative\"):\n>               im.load()\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:251: \n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\u001b[1m\u001b[31msrc/PIL/ImageFile.py\u001b[0m:266: in load\n    err_code = decoder.decode(b\"\")[1]\n\u001b[1m\u001b[31msrc/PIL/PpmImagePlugin.py\u001b[0m:291: in decode\n    self.set_as_raw(bytes(data), rawmode)\n_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ \n\nself = <PIL.PpmImagePlugin.PpmPlainDecoder object at 0x76f5c0a5f7f0>\ndata = b'\\xff', rawmode = 'RGB'\n\n    def set_as_raw(self, data: bytes, rawmode=None) -> None:\n        \"\"\"\n        Convenience method to set the internal image from a stream of raw data\n    \n        :param data: Bytes to be set\n        :param rawmode: The rawmode to be used for the decoder.\n            If not specified, it will default to the mode of the image\n        :returns: None\n        \"\"\"\n    \n        if not rawmode:\n            rawmode = self.mode\n        d = Image._getdecoder(self.mode, \"raw\", rawmode)\n        d.setimage(self.im, self.state.extents())\n        s = d.decode(data)\n    \n        if s[0] >= 0:\n            msg = \"not enough image data\"\n>           raise ValueError(msg)\n\u001b[1m\u001b[31mE           ValueError: not enough image data\u001b[0m\n\n\u001b[1m\u001b[31msrc/PIL/ImageFile.py\u001b[0m:736: ValueError\n\n\u001b[33mDuring handling of the above exception, another exception occurred:\u001b[0m\n\ntmp_path = PosixPath('/tmp/pytest-of-gcpuser/pytest-187/test_plain_ppm_value_negative0')\n\n    def test_plain_ppm_value_negative(tmp_path: Path) -> None:\n        path = str(tmp_path / \"temp.ppm\")\n        with open(path, \"wb\") as f:\n            f.write(b\"P3\\n128 128\\n255\\n-1\")\n    \n        with Image.open(path) as im:\n            with pytest.raises(ValueError, match=\"Channel value is negative\"):\n>               im.load()\n\u001b[1m\u001b[31mE               AssertionError: Regex pattern did not match.\u001b[0m\n\u001b[1m\u001b[31mE                Regex: 'Channel value is negative'\u001b[0m\n\u001b[1m\u001b[31mE                Input: 'not enough image data'\u001b[0m\n\n\u001b[1m\u001b[31mr2e_tests/test_1.py\u001b[0m:251: AssertionError\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_sanity\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P2 3 1 4 0 2 4-L-pixels0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P2 3 1 257 0 128 257-I-pixels1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P3 3 1 17 0 1 2 8 9 10 15 16 17-RGB-pixels2]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P3 3 1 257 0 1 2 128 129 130 256 257 257-RGB-pixels3]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P5 3 1 4 \\x00\\x02\\x04-L-pixels4]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P5 3 1 257 \\x00\\x00\\x00\\x80\\x01\\x01-I-pixels5]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P6 3 1 17 \\x00\\x01\\x02\\x08\\t\\n\\x0f\\x10\\x11-RGB-pixels6]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_arbitrary_maxval[P6 3 1 257 \\x00\\x00\\x00\\x01\\x00\\x02\\x00\\x80\\x00\\x81\\x00\\x82\\x01\\x00\\x01\\x01\\xff\\xff-RGB-pixels7]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_16bit_pgm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_16bit_pgm_write\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pnm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_big_endian\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 NaN \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 inf \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 -inf \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 0.0 \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_pfm_invalid[Pf 1 1 -0.0 \\x00\\x00\\x00\\x00]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain[Tests/images/hopper_1bit_plain.pbm-Tests/images/hopper_1bit.pbm]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain[Tests/images/hopper_8bit_plain.pgm-Tests/images/hopper_8bit.pgm]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain[Tests/images/hopper_8bit_plain.ppm-Tests/images/hopper_8bit.ppm]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_16bit_plain_pgm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_data_with_comment[P1\\n2 2-1010-1000000]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_data_with_comment[P2\\n3 1\\n4-0 2 4-1]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_data_with_comment[P3\\n2 2\\n255-0 0 0 001 1 1 2 2 2 255 255 255-1000000]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_truncated_data[P1\\n128 128\\n]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_truncated_data[P3\\n128 128\\n255\\n]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_invalid_data[P1\\n128 128\\n1009]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_invalid_data[P3\\n128 128\\n255\\n100A]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_token_too_long[P3\\n128 128\\n255\\n012345678910]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_token_too_long[P3\\n128 128\\n255\\n012345678910 0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_value_too_large\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_magic\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_header_with_comments\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_non_integer_token\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_header_token_too_long\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_truncated_file\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_not_enough_image_data\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_maxval[0]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_invalid_maxval[65536]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_neg_ppm\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_mimetypes\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_stdout[True]\u001b[0m\n\u001b[32mPASSED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_save_stdout[False]\u001b[0m\n\u001b[31mFAILED\u001b[0m r2e_tests/test_1.py::\u001b[1mtest_plain_ppm_value_negative\u001b[0m - AssertionError: Regex pattern did not match.\n\u001b[31m========================= \u001b[31m\u001b[1m1 failed\u001b[0m, \u001b[32m45 passed\u001b[0m\u001b[31m in 0.39s\u001b[0m\u001b[31m =========================\u001b[0m\n",
    "old_commit_res_stderr": ""
}
Tests/test_file_ppm.py src/PIL/PpmImagePlugin.py
We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
[ISSUE]
**Title:** Loading P3 PPM Images with Negative Channel Values Raises Incorrect Error

**Description:**
When attempting to load a P3 PPM image that contains negative channel values, the library raises a `ValueError` with the message "not enough image data" instead of indicating that a channel value is negative. This makes it difficult to identify the root cause of the issue when working with malformed PPM files.

**Example Buggy Code:**
```python
from PIL import Image

# Create a P3 PPM image with a negative channel value
ppm_data = b"P3\n128 128\n255\n-1"
with open("temp.ppm", "wb") as f:
    f.write(ppm_data)

# Attempt to load the image
with Image.open("temp.ppm") as im:
    im.load()
```

**Expected Behavior:**
Loading the P3 PPM image should raise a `ValueError` with a clear message indicating that a channel value is negative, such as "Channel value is negative: -1".

**Actual Behavior:**
A `ValueError` is raised with the message "not enough image data", which does not clearly indicate that the issue is due to a negative channel value.

[/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/PpmImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# PPM support for PIL
#
# History:
#       96-03-24 fl     Created
#       98-03-06 fl     Write RGBA images (as RGB, that is)
#
# Copyright (c) Secret Labs AB 1997-98.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations

import math
from typing import IO

from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
from ._binary import o32le as o32

#
# --------------------------------------------------------------------

b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d"

MODES = {
    # standard
    b"P1": "1",
    b"P2": "L",
    b"P3": "RGB",
    b"P4": "1",
    b"P5": "L",
    b"P6": "RGB",
    # extensions
    b"P0CMYK": "CMYK",
    b"Pf": "F",
    # PIL extensions (for test purposes only)
    b"PyP": "P",
    b"PyRGBA": "RGBA",
    b"PyCMYK": "CMYK",
}


def _accept(prefix: bytes) -> bool:
    return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"


##
# Image plugin for PBM, PGM, and PPM images.


class PpmImageFile(ImageFile.ImageFile):
    format = "PPM"
    format_description = "Pbmplus image"

    def _read_magic(self) -> bytes:
        assert self.fp is not None

        magic = b""
        # read until whitespace or longest available magic number
        for _ in range(6):
            c = self.fp.read(1)
            if not c or c in b_whitespace:
                break
            magic += c
        return magic

    def _read_token(self) -> bytes:
        assert self.fp is not None

        token = b""
        while len(token) <= 10:  # read until next whitespace or limit of 10 characters
            c = self.fp.read(1)
            if not c:
                break
            elif c in b_whitespace:  # token ended
                if not token:
                    # skip whitespace at start
                    continue
                break
            elif c == b"#":
                # ignores rest of the line; stops at CR, LF or EOF
                while self.fp.read(1) not in b"\r\n":
                    pass
                continue
            token += c
        if not token:
            # Token was not even 1 byte
            msg = "Reached EOF while reading header"
            raise ValueError(msg)
        elif len(token) > 10:
            msg = f"Token too long in file header: {token.decode()}"
            raise ValueError(msg)
        return token

    def _open(self) -> None:
        assert self.fp is not None

        magic_number = self._read_magic()
        try:
            mode = MODES[magic_number]
        except KeyError:
            msg = "not a PPM file"
            raise SyntaxError(msg)
        self._mode = mode

        if magic_number in (b"P1", b"P4"):
            self.custom_mimetype = "image/x-portable-bitmap"
        elif magic_number in (b"P2", b"P5"):
            self.custom_mimetype = "image/x-portable-graymap"
        elif magic_number in (b"P3", b"P6"):
            self.custom_mimetype = "image/x-portable-pixmap"

        self._size = int(self._read_token()), int(self._read_token())

        decoder_name = "raw"
        if magic_number in (b"P1", b"P2", b"P3"):
            decoder_name = "ppm_plain"

        args: str | tuple[str | int, ...]
        if mode == "1":
            args = "1;I"
        elif mode == "F":
            scale = float(self._read_token())
            if scale == 0.0 or not math.isfinite(scale):
                msg = "scale must be finite and non-zero"
                raise ValueError(msg)
            self.info["scale"] = abs(scale)

            rawmode = "F;32F" if scale < 0 else "F;32BF"
            args = (rawmode, 0, -1)
        else:
            maxval = int(self._read_token())
            if not 0 < maxval < 65536:
                msg = "maxval must be greater than 0 and less than 65536"
                raise ValueError(msg)
            if maxval > 255 and mode == "L":
                self._mode = "I"

            rawmode = mode
            if decoder_name != "ppm_plain":
                # If maxval matches a bit depth, use the raw decoder directly
                if maxval == 65535 and mode == "L":
                    rawmode = "I;16B"
                elif maxval != 255:
                    decoder_name = "ppm"

            args = rawmode if decoder_name == "raw" else (rawmode, maxval)
        self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]


#
# --------------------------------------------------------------------


class PpmPlainDecoder(ImageFile.PyDecoder):
    _pulls_fd = True
    _comment_spans: bool

    def _read_block(self) -> bytes:
        assert self.fd is not None

        return self.fd.read(ImageFile.SAFEBLOCK)

    def _find_comment_end(self, block: bytes, start: int = 0) -> int:
        a = block.find(b"\n", start)
        b = block.find(b"\r", start)
        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)

    def _ignore_comments(self, block: bytes) -> bytes:
        if self._comment_spans:
            # Finish current comment
            while block:
                comment_end = self._find_comment_end(block)
                if comment_end != -1:
                    # Comment ends in this block
                    # Delete tail of comment
                    block = block[comment_end + 1 :]
                    break
                else:
                    # Comment spans whole block
                    # So read the next block, looking for the end
                    block = self._read_block()

        # Search for any further comments
        self._comment_spans = False
        while True:
            comment_start = block.find(b"#")
            if comment_start == -1:
                # No comment found
                break
            comment_end = self._find_comment_end(block, comment_start)
            if comment_end != -1:
                # Comment ends in this block
                # Delete comment
                block = block[:comment_start] + block[comment_end + 1 :]
            else:
                # Comment continues to next block(s)
                block = block[:comment_start]
                self._comment_spans = True
                break
        return block

    def _decode_bitonal(self) -> bytearray:
        """
        This is a separate method because in the plain PBM format, all data tokens are
        exactly one byte, so the inter-token whitespace is optional.
        """
        data = bytearray()
        total_bytes = self.state.xsize * self.state.ysize

        while len(data) != total_bytes:
            block = self._read_block()  # read next block
            if not block:
                # eof
                break

            block = self._ignore_comments(block)

            tokens = b"".join(block.split())
            for token in tokens:
                if token not in (48, 49):
                    msg = b"Invalid token for this mode: %s" % bytes([token])
                    raise ValueError(msg)
            data = (data + tokens)[:total_bytes]
        invert = bytes.maketrans(b"01", b"\xFF\x00")
        return data.translate(invert)

    def _decode_blocks(self, maxval: int) -> bytearray:
        data = bytearray()
        max_len = 10
        out_byte_count = 4 if self.mode == "I" else 1
        out_max = 65535 if self.mode == "I" else 255
        bands = Image.getmodebands(self.mode)
        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count

        half_token = b""
        while len(data) != total_bytes:
            block = self._read_block()  # read next block
            if not block:
                if half_token:
                    block = bytearray(b" ")  # flush half_token
                else:
                    # eof
                    break

            block = self._ignore_comments(block)

            if half_token:
                block = half_token + block  # stitch half_token to new block
                half_token = b""

            tokens = block.split()

            if block and not block[-1:].isspace():  # block might split token
                half_token = tokens.pop()  # save half token for later
                if len(half_token) > max_len:  # prevent buildup of half_token
                    msg = (
                        b"Token too long found in data: %s" % half_token[: max_len + 1]
                    )
                    raise ValueError(msg)

            for token in tokens:
                if len(token) > max_len:
                    msg = b"Token too long found in data: %s" % token[: max_len + 1]
                    raise ValueError(msg)
                value = int(token)
                if value > maxval:
                    msg_str = f"Channel value too large for this mode: {value}"
                    raise ValueError(msg_str)
                value = round(value / maxval * out_max)
                data += o32(value) if self.mode == "I" else o8(value)
                if len(data) == total_bytes:  # finished!
                    break
        return data

    def decode(self, buffer: bytes) -> tuple[int, int]:
        self._comment_spans = False
        if self.mode == "1":
            data = self._decode_bitonal()
            rawmode = "1;8"
        else:
            maxval = self.args[-1]
            data = self._decode_blocks(maxval)
            rawmode = "I;32" if self.mode == "I" else self.mode
        self.set_as_raw(bytes(data), rawmode)
        return -1, 0


class PpmDecoder(ImageFile.PyDecoder):
    _pulls_fd = True

    def decode(self, buffer: bytes) -> tuple[int, int]:
        assert self.fd is not None

        data = bytearray()
        maxval = self.args[-1]
        in_byte_count = 1 if maxval < 256 else 2
        out_byte_count = 4 if self.mode == "I" else 1
        out_max = 65535 if self.mode == "I" else 255
        bands = Image.getmodebands(self.mode)
        while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:
            pixels = self.fd.read(in_byte_count * bands)
            if len(pixels) < in_byte_count * bands:
                # eof
                break
            for b in range(bands):
                value = (
                    pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)
                )
                value = min(out_max, round(value / maxval * out_max))
                data += o32(value) if self.mode == "I" else o8(value)
        rawmode = "I;32" if self.mode == "I" else self.mode
        self.set_as_raw(bytes(data), rawmode)
        return -1, 0


#
# --------------------------------------------------------------------


def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
    if im.mode == "1":
        rawmode, head = "1;I", b"P4"
    elif im.mode == "L":
        rawmode, head = "L", b"P5"
    elif im.mode == "I":
        rawmode, head = "I;16B", b"P5"
    elif im.mode in ("RGB", "RGBA"):
        rawmode, head = "RGB", b"P6"
    elif im.mode == "F":
        rawmode, head = "F;32F", b"Pf"
    else:
        msg = f"cannot write mode {im.mode} as PPM"
        raise OSError(msg)
    fp.write(head + b"\n%d %d\n" % im.size)
    if head == b"P6":
        fp.write(b"255\n")
    elif head == b"P5":
        if rawmode == "L":
            fp.write(b"255\n")
        else:
            fp.write(b"65535\n")
    elif head == b"Pf":
        fp.write(b"-1.0\n")
    row_order = -1 if im.mode == "F" else 1
    ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])


#
# --------------------------------------------------------------------


Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
Image.register_save(PpmImageFile.format, _save)

Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder)

Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])

Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")

```
--- 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:** Loading P3 PPM Images with Negative Channel Values Raises Incorrect Error

**Description:**
When attempting to load a P3 PPM image that contains negative channel values, the library raises a `ValueError` with the message "not enough image data" instead of indicating that a channel value is negative. This makes it difficult to identify the root cause of the issue when working with malformed PPM files.

**Example Buggy Code:**
```python
from PIL import Image

# Create a P3 PPM image with a negative channel value
ppm_data = b"P3\n128 128\n255\n-1"
with open("temp.ppm", "wb") as f:
    f.write(ppm_data)

# Attempt to load the image
with Image.open("temp.ppm") as im:
    im.load()
```

**Expected Behavior:**
Loading the P3 PPM image should raise a `ValueError` with a clear message indicating that a channel value is negative, such as "Channel value is negative: -1".

**Actual Behavior:**
A `ValueError` is raised with the message "not enough image data", which does not clearly indicate that the issue is due to a negative channel value.

[/ISSUE]
{
    "\u001b[1mtest_sanity\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P2 3 1 4 0 2 4-L-pixels0]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P2 3 1 257 0 128 257-I-pixels1]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P3 3 1 17 0 1 2 8 9 10 15 16 17-RGB-pixels2]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P3 3 1 257 0 1 2 128 129 130 256 257 257-RGB-pixels3]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P5 3 1 4 \\x00\\x02\\x04-L-pixels4]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P5 3 1 257 \\x00\\x00\\x00\\x80\\x01\\x01-I-pixels5]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P6 3 1 17 \\x00\\x01\\x02\\x08\\t\\n\\x0f\\x10\\x11-RGB-pixels6]\u001b[0m": "PASSED",
    "\u001b[1mtest_arbitrary_maxval[P6 3 1 257 \\x00\\x00\\x00\\x01\\x00\\x02\\x00\\x80\\x00\\x81\\x00\\x82\\x01\\x00\\x01\\x01\\xff\\xff-RGB-pixels7]\u001b[0m": "PASSED",
    "\u001b[1mtest_16bit_pgm\u001b[0m": "PASSED",
    "\u001b[1mtest_16bit_pgm_write\u001b[0m": "PASSED",
    "\u001b[1mtest_pnm\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm_big_endian\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm_invalid[Pf 1 1 NaN \\x00\\x00\\x00\\x00]\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm_invalid[Pf 1 1 inf \\x00\\x00\\x00\\x00]\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm_invalid[Pf 1 1 -inf \\x00\\x00\\x00\\x00]\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm_invalid[Pf 1 1 0.0 \\x00\\x00\\x00\\x00]\u001b[0m": "PASSED",
    "\u001b[1mtest_pfm_invalid[Pf 1 1 -0.0 \\x00\\x00\\x00\\x00]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain[Tests/images/hopper_1bit_plain.pbm-Tests/images/hopper_1bit.pbm]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain[Tests/images/hopper_8bit_plain.pgm-Tests/images/hopper_8bit.pgm]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain[Tests/images/hopper_8bit_plain.ppm-Tests/images/hopper_8bit.ppm]\u001b[0m": "PASSED",
    "\u001b[1mtest_16bit_plain_pgm\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_data_with_comment[P1\\n2 2-1010-1000000]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_data_with_comment[P2\\n3 1\\n4-0 2 4-1]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_data_with_comment[P3\\n2 2\\n255-0 0 0 001 1 1 2 2 2 255 255 255-1000000]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_truncated_data[P1\\n128 128\\n]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_truncated_data[P3\\n128 128\\n255\\n]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_invalid_data[P1\\n128 128\\n1009]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_invalid_data[P3\\n128 128\\n255\\n100A]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_ppm_token_too_long[P3\\n128 128\\n255\\n012345678910]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_ppm_token_too_long[P3\\n128 128\\n255\\n012345678910 0]\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_ppm_value_negative\u001b[0m": "PASSED",
    "\u001b[1mtest_plain_ppm_value_too_large\u001b[0m": "PASSED",
    "\u001b[1mtest_magic\u001b[0m": "PASSED",
    "\u001b[1mtest_header_with_comments\u001b[0m": "PASSED",
    "\u001b[1mtest_non_integer_token\u001b[0m": "PASSED",
    "\u001b[1mtest_header_token_too_long\u001b[0m": "PASSED",
    "\u001b[1mtest_truncated_file\u001b[0m": "PASSED",
    "\u001b[1mtest_not_enough_image_data\u001b[0m": "PASSED",
    "\u001b[1mtest_invalid_maxval[0]\u001b[0m": "PASSED",
    "\u001b[1mtest_invalid_maxval[65536]\u001b[0m": "PASSED",
    "\u001b[1mtest_neg_ppm\u001b[0m": "PASSED",
    "\u001b[1mtest_mimetypes\u001b[0m": "PASSED",
    "\u001b[1mtest_save_stdout[True]\u001b[0m": "PASSED",
    "\u001b[1mtest_save_stdout[False]\u001b[0m": "PASSED"
}
diff --git a/src/PIL/PpmImagePlugin.py b/src/PIL/PpmImagePlugin.py
index 6ac7a9bbc..bca3018c3 100644
--- a/src/PIL/PpmImagePlugin.py
+++ b/src/PIL/PpmImagePlugin.py
@@ -270,6 +270,9 @@ class PpmPlainDecoder(ImageFile.PyDecoder):
                     msg = b"Token too long found in data: %s" % token[: max_len + 1]
                     raise ValueError(msg)
                 value = int(token)
+                if value < 0:
+                    msg_str = f"Channel value is negative: {value}"
+                    raise ValueError(msg_str)
                 if value > maxval:
                     msg_str = f"Channel value too large for this mode: {value}"
                     raise ValueError(msg_str)
 

{"src/PIL/PpmImagePlugin.py": "#\n# The Python Imaging Library.\n# $Id$\n#\n# PPM support for PIL\n#\n# History:\n#       96-03-24 fl     Created\n#       98-03-06 fl     Write RGBA images (as RGB, that is)\n#\n# Copyright (c) Secret Labs AB 1997-98.\n# Copyright (c) Fredrik Lundh 1996.\n#\n# See the README file for information on usage and redistribution.\n#\nfrom __future__ import annotations\n\nimport math\nfrom typing import IO\n\nfrom . import Image, ImageFile\nfrom ._binary import i16be as i16\nfrom ._binary import o8\nfrom ._binary import o32le as o32\n\n#\n# --------------------------------------------------------------------\n\nb_whitespace = b\"\\x20\\x09\\x0a\\x0b\\x0c\\x0d\"\n\nMODES = {\n    # standard\n    b\"P1\": \"1\",\n    b\"P2\": \"L\",\n    b\"P3\": \"RGB\",\n    b\"P4\": \"1\",\n    b\"P5\": \"L\",\n    b\"P6\": \"RGB\",\n    # extensions\n    b\"P0CMYK\": \"CMYK\",\n    b\"Pf\": \"F\",\n    # PIL extensions (for test purposes only)\n    b\"PyP\": \"P\",\n    b\"PyRGBA\": \"RGBA\",\n    b\"PyCMYK\": \"CMYK\",\n}\n\n\ndef _accept(prefix: bytes) -> bool:\n    return prefix[0:1] == b\"P\" and prefix[1] in b\"0123456fy\"\n\n\n##\n# Image plugin for PBM, PGM, and PPM images.\n\n\nclass PpmImageFile(ImageFile.ImageFile):\n    format = \"PPM\"\n    format_description = \"Pbmplus image\"\n\n    def _read_magic(self) -> bytes:\n        assert self.fp is not None\n\n        magic = b\"\"\n        # read until whitespace or longest available magic number\n        for _ in range(6):\n            c = self.fp.read(1)\n            if not c or c in b_whitespace:\n                break\n            magic += c\n        return magic\n\n    def _read_token(self) -> bytes:\n        assert self.fp is not None\n\n        token = b\"\"\n        while len(token) <= 10:  # read until next whitespace or limit of 10 characters\n            c = self.fp.read(1)\n            if not c:\n                break\n            elif c in b_whitespace:  # token ended\n                if not token:\n                    # skip whitespace at start\n                    continue\n                break\n            elif c == b\"#\":\n                # ignores rest of the line; stops at CR, LF or EOF\n                while self.fp.read(1) not in b\"\\r\\n\":\n                    pass\n                continue\n            token += c\n        if not token:\n            # Token was not even 1 byte\n            msg = \"Reached EOF while reading header\"\n            raise ValueError(msg)\n        elif len(token) > 10:\n            msg = f\"Token too long in file header: {token.decode()}\"\n            raise ValueError(msg)\n        return token\n\n    def _open(self) -> None:\n        assert self.fp is not None\n\n        magic_number = self._read_magic()\n        try:\n            mode = MODES[magic_number]\n        except KeyError:\n            msg = \"not a PPM file\"\n            raise SyntaxError(msg)\n        self._mode = mode\n\n        if magic_number in (b\"P1\", b\"P4\"):\n            self.custom_mimetype = \"image/x-portable-bitmap\"\n        elif magic_number in (b\"P2\", b\"P5\"):\n            self.custom_mimetype = \"image/x-portable-graymap\"\n        elif magic_number in (b\"P3\", b\"P6\"):\n            self.custom_mimetype = \"image/x-portable-pixmap\"\n\n        self._size = int(self._read_token()), int(self._read_token())\n\n        decoder_name = \"raw\"\n        if magic_number in (b\"P1\", b\"P2\", b\"P3\"):\n            decoder_name = \"ppm_plain\"\n\n        args: str | tuple[str | int, ...]\n        if mode == \"1\":\n            args = \"1;I\"\n        elif mode == \"F\":\n            scale = float(self._read_token())\n            if scale == 0.0 or not math.isfinite(scale):\n                msg = \"scale must be finite and non-zero\"\n                raise ValueError(msg)\n            self.info[\"scale\"] = abs(scale)\n\n            rawmode = \"F;32F\" if scale < 0 else \"F;32BF\"\n            args = (rawmode, 0, -1)\n        else:\n            maxval = int(self._read_token())\n            if not 0 < maxval < 65536:\n                msg = \"maxval must be greater than 0 and less than 65536\"\n                raise ValueError(msg)\n            if maxval > 255 and mode == \"L\":\n                self._mode = \"I\"\n\n            rawmode = mode\n            if decoder_name != \"ppm_plain\":\n                # If maxval matches a bit depth, use the raw decoder directly\n                if maxval == 65535 and mode == \"L\":\n                    rawmode = \"I;16B\"\n                elif maxval != 255:\n                    decoder_name = \"ppm\"\n\n            args = rawmode if decoder_name == \"raw\" else (rawmode, maxval)\n        self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]\n\n\n#\n# --------------------------------------------------------------------\n\n\nclass PpmPlainDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n    _comment_spans: bool\n\n    def _read_block(self) -> bytes:\n        assert self.fd is not None\n\n        return self.fd.read(ImageFile.SAFEBLOCK)\n\n    def _find_comment_end(self, block: bytes, start: int = 0) -> int:\n        a = block.find(b\"\\n\", start)\n        b = block.find(b\"\\r\", start)\n        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)\n\n    def _ignore_comments(self, block: bytes) -> bytes:\n        if self._comment_spans:\n            # Finish current comment\n            while block:\n                comment_end = self._find_comment_end(block)\n                if comment_end != -1:\n                    # Comment ends in this block\n                    # Delete tail of comment\n                    block = block[comment_end + 1 :]\n                    break\n                else:\n                    # Comment spans whole block\n                    # So read the next block, looking for the end\n                    block = self._read_block()\n\n        # Search for any further comments\n        self._comment_spans = False\n        while True:\n            comment_start = block.find(b\"#\")\n            if comment_start == -1:\n                # No comment found\n                break\n            comment_end = self._find_comment_end(block, comment_start)\n            if comment_end != -1:\n                # Comment ends in this block\n                # Delete comment\n                block = block[:comment_start] + block[comment_end + 1 :]\n            else:\n                # Comment continues to next block(s)\n                block = block[:comment_start]\n                self._comment_spans = True\n                break\n        return block\n\n    def _decode_bitonal(self) -> bytearray:\n        \"\"\"\n        This is a separate method because in the plain PBM format, all data tokens are\n        exactly one byte, so the inter-token whitespace is optional.\n        \"\"\"\n        data = bytearray()\n        total_bytes = self.state.xsize * self.state.ysize\n\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                # eof\n                break\n\n            block = self._ignore_comments(block)\n\n            tokens = b\"\".join(block.split())\n            for token in tokens:\n                if token not in (48, 49):\n                    msg = b\"Invalid token for this mode: %s\" % bytes([token])\n                    raise ValueError(msg)\n            data = (data + tokens)[:total_bytes]\n        invert = bytes.maketrans(b\"01\", b\"\\xFF\\x00\")\n        return data.translate(invert)\n\n    def _decode_blocks(self, maxval: int) -> bytearray:\n        data = bytearray()\n        max_len = 10\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count\n\n        half_token = b\"\"\n        while len(data) != total_bytes:\n            block = self._read_block()  # read next block\n            if not block:\n                if half_token:\n                    block = bytearray(b\" \")  # flush half_token\n                else:\n                    # eof\n                    break\n\n            block = self._ignore_comments(block)\n\n            if half_token:\n                block = half_token + block  # stitch half_token to new block\n                half_token = b\"\"\n\n            tokens = block.split()\n\n            if block and not block[-1:].isspace():  # block might split token\n                half_token = tokens.pop()  # save half token for later\n                if len(half_token) > max_len:  # prevent buildup of half_token\n                    msg = (\n                        b\"Token too long found in data: %s\" % half_token[: max_len + 1]\n                    )\n                    raise ValueError(msg)\n\n            for token in tokens:\n                if len(token) > max_len:\n                    msg = b\"Token too long found in data: %s\" % token[: max_len + 1]\n                    raise ValueError(msg)\n                value = int(token)\n                if value > maxval:\n                    msg_str = f\"Channel value too large for this mode: {value}\"\n                    raise ValueError(msg_str)\n                value = round(value / maxval * out_max)\n                data += o32(value) if self.mode == \"I\" else o8(value)\n                if len(data) == total_bytes:  # finished!\n                    break\n        return data\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        self._comment_spans = False\n        if self.mode == \"1\":\n            data = self._decode_bitonal()\n            rawmode = \"1;8\"\n        else:\n            maxval = self.args[-1]\n            data = self._decode_blocks(maxval)\n            rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0\n\n\nclass PpmDecoder(ImageFile.PyDecoder):\n    _pulls_fd = True\n\n    def decode(self, buffer: bytes) -> tuple[int, int]:\n        assert self.fd is not None\n\n        data = bytearray()\n        maxval = self.args[-1]\n        in_byte_count = 1 if maxval < 256 else 2\n        out_byte_count = 4 if self.mode == \"I\" else 1\n        out_max = 65535 if self.mode == \"I\" else 255\n        bands = Image.getmodebands(self.mode)\n        while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:\n            pixels = self.fd.read(in_byte_count * bands)\n            if len(pixels) < in_byte_count * bands:\n                # eof\n                break\n            for b in range(bands):\n                value = (\n                    pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)\n                )\n                value = min(out_max, round(value / maxval * out_max))\n                data += o32(value) if self.mode == \"I\" else o8(value)\n        rawmode = \"I;32\" if self.mode == \"I\" else self.mode\n        self.set_as_raw(bytes(data), rawmode)\n        return -1, 0\n\n\n#\n# --------------------------------------------------------------------\n\n\ndef _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:\n    if im.mode == \"1\":\n        rawmode, head = \"1;I\", b\"P4\"\n    elif im.mode == \"L\":\n        rawmode, head = \"L\", b\"P5\"\n    elif im.mode == \"I\":\n        rawmode, head = \"I;16B\", b\"P5\"\n    elif im.mode in (\"RGB\", \"RGBA\"):\n        rawmode, head = \"RGB\", b\"P6\"\n    elif im.mode == \"F\":\n        rawmode, head = \"F;32F\", b\"Pf\"\n    else:\n        msg = f\"cannot write mode {im.mode} as PPM\"\n        raise OSError(msg)\n    fp.write(head + b\"\\n%d %d\\n\" % im.size)\n    if head == b\"P6\":\n        fp.write(b\"255\\n\")\n    elif head == b\"P5\":\n        if rawmode == \"L\":\n            fp.write(b\"255\\n\")\n        else:\n            fp.write(b\"65535\\n\")\n    elif head == b\"Pf\":\n        fp.write(b\"-1.0\\n\")\n    row_order = -1 if im.mode == \"F\" else 1\n    ImageFile._save(im, fp, [(\"raw\", (0, 0) + im.size, 0, (rawmode, 0, row_order))])\n\n\n#\n# --------------------------------------------------------------------\n\n\nImage.register_open(PpmImageFile.format, PpmImageFile, _accept)\nImage.register_save(PpmImageFile.format, _save)\n\nImage.register_decoder(\"ppm\", PpmDecoder)\nImage.register_decoder(\"ppm_plain\", PpmPlainDecoder)\n\nImage.register_extensions(PpmImageFile.format, [\".pbm\", \".pgm\", \".ppm\", \".pnm\", \".pfm\"])\n\nImage.register_mime(PpmImageFile.format, \"image/x-portable-anymap\")\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:** Loading P3 PPM Images with Negative Channel Values Raises Incorrect Error

**Description:**
When attempting to load a P3 PPM image that contains negative channel values, the library raises a `ValueError` with the message "not enough image data" instead of indicating that a channel value is negative. This makes it difficult to identify the root cause of the issue when working with malformed PPM files.

**Example Buggy Code:**
```python
from PIL import Image

# Create a P3 PPM image with a negative channel value
ppm_data = b"P3\n128 128\n255\n-1"
with open("temp.ppm", "wb") as f:
    f.write(ppm_data)

# Attempt to load the image
with Image.open("temp.ppm") as im:
    im.load()
```

**Expected Behavior:**
Loading the P3 PPM image should raise a `ValueError` with a clear message indicating that a channel value is negative, such as "Channel value is negative: -1".

**Actual Behavior:**
A `ValueError` is raised with the message "not enough image data", which does not clearly indicate that the issue is due to a negative channel value.

[/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/PpmImagePlugin.py
#
# The Python Imaging Library.
# $Id$
#
# PPM support for PIL
#
# History:
#       96-03-24 fl     Created
#       98-03-06 fl     Write RGBA images (as RGB, that is)
#
# Copyright (c) Secret Labs AB 1997-98.
# Copyright (c) Fredrik Lundh 1996.
#
# See the README file for information on usage and redistribution.
#
from __future__ import annotations

import math
from typing import IO

from . import Image, ImageFile
from ._binary import i16be as i16
from ._binary import o8
from ._binary import o32le as o32

#
# --------------------------------------------------------------------

b_whitespace = b"\x20\x09\x0a\x0b\x0c\x0d"

MODES = {
    # standard
    b"P1": "1",
    b"P2": "L",
    b"P3": "RGB",
    b"P4": "1",
    b"P5": "L",
    b"P6": "RGB",
    # extensions
    b"P0CMYK": "CMYK",
    b"Pf": "F",
    # PIL extensions (for test purposes only)
    b"PyP": "P",
    b"PyRGBA": "RGBA",
    b"PyCMYK": "CMYK",
}


def _accept(prefix: bytes) -> bool:
    return prefix[0:1] == b"P" and prefix[1] in b"0123456fy"


##
# Image plugin for PBM, PGM, and PPM images.


class PpmImageFile(ImageFile.ImageFile):
    format = "PPM"
    format_description = "Pbmplus image"

    def _read_magic(self) -> bytes:
        assert self.fp is not None

        magic = b""
        # read until whitespace or longest available magic number
        for _ in range(6):
            c = self.fp.read(1)
            if not c or c in b_whitespace:
                break
            magic += c
        return magic

    def _read_token(self) -> bytes:
        assert self.fp is not None

        token = b""
        while len(token) <= 10:  # read until next whitespace or limit of 10 characters
            c = self.fp.read(1)
            if not c:
                break
            elif c in b_whitespace:  # token ended
                if not token:
                    # skip whitespace at start
                    continue
                break
            elif c == b"#":
                # ignores rest of the line; stops at CR, LF or EOF
                while self.fp.read(1) not in b"\r\n":
                    pass
                continue
            token += c
        if not token:
            # Token was not even 1 byte
            msg = "Reached EOF while reading header"
            raise ValueError(msg)
        elif len(token) > 10:
            msg = f"Token too long in file header: {token.decode()}"
            raise ValueError(msg)
        return token

    def _open(self) -> None:
        assert self.fp is not None

        magic_number = self._read_magic()
        try:
            mode = MODES[magic_number]
        except KeyError:
            msg = "not a PPM file"
            raise SyntaxError(msg)
        self._mode = mode

        if magic_number in (b"P1", b"P4"):
            self.custom_mimetype = "image/x-portable-bitmap"
        elif magic_number in (b"P2", b"P5"):
            self.custom_mimetype = "image/x-portable-graymap"
        elif magic_number in (b"P3", b"P6"):
            self.custom_mimetype = "image/x-portable-pixmap"

        self._size = int(self._read_token()), int(self._read_token())

        decoder_name = "raw"
        if magic_number in (b"P1", b"P2", b"P3"):
            decoder_name = "ppm_plain"

        args: str | tuple[str | int, ...]
        if mode == "1":
            args = "1;I"
        elif mode == "F":
            scale = float(self._read_token())
            if scale == 0.0 or not math.isfinite(scale):
                msg = "scale must be finite and non-zero"
                raise ValueError(msg)
            self.info["scale"] = abs(scale)

            rawmode = "F;32F" if scale < 0 else "F;32BF"
            args = (rawmode, 0, -1)
        else:
            maxval = int(self._read_token())
            if not 0 < maxval < 65536:
                msg = "maxval must be greater than 0 and less than 65536"
                raise ValueError(msg)
            if maxval > 255 and mode == "L":
                self._mode = "I"

            rawmode = mode
            if decoder_name != "ppm_plain":
                # If maxval matches a bit depth, use the raw decoder directly
                if maxval == 65535 and mode == "L":
                    rawmode = "I;16B"
                elif maxval != 255:
                    decoder_name = "ppm"

            args = rawmode if decoder_name == "raw" else (rawmode, maxval)
        self.tile = [(decoder_name, (0, 0) + self.size, self.fp.tell(), args)]


#
# --------------------------------------------------------------------


class PpmPlainDecoder(ImageFile.PyDecoder):
    _pulls_fd = True
    _comment_spans: bool

    def _read_block(self) -> bytes:
        assert self.fd is not None

        return self.fd.read(ImageFile.SAFEBLOCK)

    def _find_comment_end(self, block: bytes, start: int = 0) -> int:
        a = block.find(b"\n", start)
        b = block.find(b"\r", start)
        return min(a, b) if a * b > 0 else max(a, b)  # lowest nonnegative index (or -1)

    def _ignore_comments(self, block: bytes) -> bytes:
        if self._comment_spans:
            # Finish current comment
            while block:
                comment_end = self._find_comment_end(block)
                if comment_end != -1:
                    # Comment ends in this block
                    # Delete tail of comment
                    block = block[comment_end + 1 :]
                    break
                else:
                    # Comment spans whole block
                    # So read the next block, looking for the end
                    block = self._read_block()

        # Search for any further comments
        self._comment_spans = False
        while True:
            comment_start = block.find(b"#")
            if comment_start == -1:
                # No comment found
                break
            comment_end = self._find_comment_end(block, comment_start)
            if comment_end != -1:
                # Comment ends in this block
                # Delete comment
                block = block[:comment_start] + block[comment_end + 1 :]
            else:
                # Comment continues to next block(s)
                block = block[:comment_start]
                self._comment_spans = True
                break
        return block

    def _decode_bitonal(self) -> bytearray:
        """
        This is a separate method because in the plain PBM format, all data tokens are
        exactly one byte, so the inter-token whitespace is optional.
        """
        data = bytearray()
        total_bytes = self.state.xsize * self.state.ysize

        while len(data) != total_bytes:
            block = self._read_block()  # read next block
            if not block:
                # eof
                break

            block = self._ignore_comments(block)

            tokens = b"".join(block.split())
            for token in tokens:
                if token not in (48, 49):
                    msg = b"Invalid token for this mode: %s" % bytes([token])
                    raise ValueError(msg)
            data = (data + tokens)[:total_bytes]
        invert = bytes.maketrans(b"01", b"\xFF\x00")
        return data.translate(invert)

    def _decode_blocks(self, maxval: int) -> bytearray:
        data = bytearray()
        max_len = 10
        out_byte_count = 4 if self.mode == "I" else 1
        out_max = 65535 if self.mode == "I" else 255
        bands = Image.getmodebands(self.mode)
        total_bytes = self.state.xsize * self.state.ysize * bands * out_byte_count

        half_token = b""
        while len(data) != total_bytes:
            block = self._read_block()  # read next block
            if not block:
                if half_token:
                    block = bytearray(b" ")  # flush half_token
                else:
                    # eof
                    break

            block = self._ignore_comments(block)

            if half_token:
                block = half_token + block  # stitch half_token to new block
                half_token = b""

            tokens = block.split()

            if block and not block[-1:].isspace():  # block might split token
                half_token = tokens.pop()  # save half token for later
                if len(half_token) > max_len:  # prevent buildup of half_token
                    msg = (
                        b"Token too long found in data: %s" % half_token[: max_len + 1]
                    )
                    raise ValueError(msg)

            for token in tokens:
                if len(token) > max_len:
                    msg = b"Token too long found in data: %s" % token[: max_len + 1]
                    raise ValueError(msg)
                value = int(token)
                if value > maxval:
                    msg_str = f"Channel value too large for this mode: {value}"
                    raise ValueError(msg_str)
                value = round(value / maxval * out_max)
                data += o32(value) if self.mode == "I" else o8(value)
                if len(data) == total_bytes:  # finished!
                    break
        return data

    def decode(self, buffer: bytes) -> tuple[int, int]:
        self._comment_spans = False
        if self.mode == "1":
            data = self._decode_bitonal()
            rawmode = "1;8"
        else:
            maxval = self.args[-1]
            data = self._decode_blocks(maxval)
            rawmode = "I;32" if self.mode == "I" else self.mode
        self.set_as_raw(bytes(data), rawmode)
        return -1, 0


class PpmDecoder(ImageFile.PyDecoder):
    _pulls_fd = True

    def decode(self, buffer: bytes) -> tuple[int, int]:
        assert self.fd is not None

        data = bytearray()
        maxval = self.args[-1]
        in_byte_count = 1 if maxval < 256 else 2
        out_byte_count = 4 if self.mode == "I" else 1
        out_max = 65535 if self.mode == "I" else 255
        bands = Image.getmodebands(self.mode)
        while len(data) < self.state.xsize * self.state.ysize * bands * out_byte_count:
            pixels = self.fd.read(in_byte_count * bands)
            if len(pixels) < in_byte_count * bands:
                # eof
                break
            for b in range(bands):
                value = (
                    pixels[b] if in_byte_count == 1 else i16(pixels, b * in_byte_count)
                )
                value = min(out_max, round(value / maxval * out_max))
                data += o32(value) if self.mode == "I" else o8(value)
        rawmode = "I;32" if self.mode == "I" else self.mode
        self.set_as_raw(bytes(data), rawmode)
        return -1, 0


#
# --------------------------------------------------------------------


def _save(im: Image.Image, fp: IO[bytes], filename: str) -> None:
    if im.mode == "1":
        rawmode, head = "1;I", b"P4"
    elif im.mode == "L":
        rawmode, head = "L", b"P5"
    elif im.mode == "I":
        rawmode, head = "I;16B", b"P5"
    elif im.mode in ("RGB", "RGBA"):
        rawmode, head = "RGB", b"P6"
    elif im.mode == "F":
        rawmode, head = "F;32F", b"Pf"
    else:
        msg = f"cannot write mode {im.mode} as PPM"
        raise OSError(msg)
    fp.write(head + b"\n%d %d\n" % im.size)
    if head == b"P6":
        fp.write(b"255\n")
    elif head == b"P5":
        if rawmode == "L":
            fp.write(b"255\n")
        else:
            fp.write(b"65535\n")
    elif head == b"Pf":
        fp.write(b"-1.0\n")
    row_order = -1 if im.mode == "F" else 1
    ImageFile._save(im, fp, [("raw", (0, 0) + im.size, 0, (rawmode, 0, row_order))])


#
# --------------------------------------------------------------------


Image.register_open(PpmImageFile.format, PpmImageFile, _accept)
Image.register_save(PpmImageFile.format, _save)

Image.register_decoder("ppm", PpmDecoder)
Image.register_decoder("ppm_plain", PpmPlainDecoder)

Image.register_extensions(PpmImageFile.format, [".pbm", ".pgm", ".ppm", ".pnm", ".pfm"])

Image.register_mime(PpmImageFile.format, "image/x-portable-anymap")

```
--- 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