diff --git a/Tests/test_file_pdf.py b/Tests/test_file_pdf.py index 99ef39147ab..ea3b6c1d9f3 100644 --- a/Tests/test_file_pdf.py +++ b/Tests/test_file_pdf.py @@ -7,267 +7,280 @@ import pytest from PIL import Image, PdfParser -from .helper import PillowTestCase, hopper +from .helper import hopper -class TestFilePdf(PillowTestCase): - def helper_save_as_pdf(self, mode, **kwargs): - # Arrange - im = hopper(mode) - outfile = self.tempfile("temp_" + mode + ".pdf") +def helper_save_as_pdf(tmp_path, mode, **kwargs): + # Arrange + im = hopper(mode) + outfile = str(tmp_path / ("temp_" + mode + ".pdf")) - # Act - im.save(outfile, **kwargs) + # Act + im.save(outfile, **kwargs) - # Assert - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - with PdfParser.PdfParser(outfile) as pdf: - if kwargs.get("append_images", False) or kwargs.get("append", False): - assert len(pdf.pages) > 1 - else: - assert len(pdf.pages) > 0 - with open(outfile, "rb") as fp: - contents = fp.read() - size = tuple( - int(d) - for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() - ) - assert im.size == size + # Assert + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + with PdfParser.PdfParser(outfile) as pdf: + if kwargs.get("append_images", False) or kwargs.get("append", False): + assert len(pdf.pages) > 1 + else: + assert len(pdf.pages) > 0 + with open(outfile, "rb") as fp: + contents = fp.read() + size = tuple( + int(d) for d in contents.split(b"/MediaBox [ 0 0 ")[1].split(b"]")[0].split() + ) + assert im.size == size + + return outfile - return outfile - def test_monochrome(self): - # Arrange - mode = "1" +def test_monochrome(tmp_path): + # Arrange + mode = "1" - # Act / Assert - self.helper_save_as_pdf(mode) + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - def test_greyscale(self): - # Arrange - mode = "L" - # Act / Assert - self.helper_save_as_pdf(mode) +def test_greyscale(tmp_path): + # Arrange + mode = "L" - def test_rgb(self): - # Arrange - mode = "RGB" + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - # Act / Assert - self.helper_save_as_pdf(mode) - def test_p_mode(self): - # Arrange - mode = "P" +def test_rgb(tmp_path): + # Arrange + mode = "RGB" - # Act / Assert - self.helper_save_as_pdf(mode) + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - def test_cmyk_mode(self): - # Arrange - mode = "CMYK" - # Act / Assert - self.helper_save_as_pdf(mode) +def test_p_mode(tmp_path): + # Arrange + mode = "P" - def test_unsupported_mode(self): - im = hopper("LA") - outfile = self.tempfile("temp_LA.pdf") + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - with pytest.raises(ValueError): - im.save(outfile) - def test_save_all(self): - # Single frame image - self.helper_save_as_pdf("RGB", save_all=True) +def test_cmyk_mode(tmp_path): + # Arrange + mode = "CMYK" - # Multiframe image - with Image.open("Tests/images/dispose_bgnd.gif") as im: + # Act / Assert + helper_save_as_pdf(tmp_path, mode) - outfile = self.tempfile("temp.pdf") - im.save(outfile, save_all=True) - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 +def test_unsupported_mode(tmp_path): + im = hopper("LA") + outfile = str(tmp_path / "temp_LA.pdf") - # Append images - ims = [hopper()] - im.copy().save(outfile, save_all=True, append_images=ims) + with pytest.raises(ValueError): + im.save(outfile) - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 - # Test appending using a generator - def imGenerator(ims): - yield from ims +def test_save_all(tmp_path): + # Single frame image + helper_save_as_pdf(tmp_path, "RGB", save_all=True) - im.save(outfile, save_all=True, append_images=imGenerator(ims)) + # Multiframe image + with Image.open("Tests/images/dispose_bgnd.gif") as im: + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile, save_all=True) assert os.path.isfile(outfile) assert os.path.getsize(outfile) > 0 - # Append JPEG images - with Image.open("Tests/images/flower.jpg") as jpeg: - jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) + # Append images + ims = [hopper()] + im.copy().save(outfile, save_all=True, append_images=ims) assert os.path.isfile(outfile) assert os.path.getsize(outfile) > 0 - def test_multiframe_normal_save(self): - # Test saving a multiframe image without save_all - with Image.open("Tests/images/dispose_bgnd.gif") as im: + # Test appending using a generator + def imGenerator(ims): + yield from ims - outfile = self.tempfile("temp.pdf") - im.save(outfile) + im.save(outfile, save_all=True, append_images=imGenerator(ims)) - assert os.path.isfile(outfile) - assert os.path.getsize(outfile) > 0 + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - def test_pdf_open(self): - # fail on a buffer full of null bytes - with pytest.raises(PdfParser.PdfFormatError): - PdfParser.PdfParser(buf=bytearray(65536)) + # Append JPEG images + with Image.open("Tests/images/flower.jpg") as jpeg: + jpeg.save(outfile, save_all=True, append_images=[jpeg.copy()]) - # make an empty PDF object - with PdfParser.PdfParser() as empty_pdf: - assert len(empty_pdf.pages) == 0 - assert len(empty_pdf.info) == 0 - assert not empty_pdf.should_close_buf - assert not empty_pdf.should_close_file + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 - # make a PDF file - pdf_filename = self.helper_save_as_pdf("RGB") - # open the PDF file - with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: - assert len(hopper_pdf.pages) == 1 - assert hopper_pdf.should_close_buf - assert hopper_pdf.should_close_file +def test_multiframe_normal_save(tmp_path): + # Test saving a multiframe image without save_all + with Image.open("Tests/images/dispose_bgnd.gif") as im: + + outfile = str(tmp_path / "temp.pdf") + im.save(outfile) - # read a PDF file from a buffer with a non-zero offset - with open(pdf_filename, "rb") as f: - content = b"xyzzy" + f.read() - with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: + assert os.path.isfile(outfile) + assert os.path.getsize(outfile) > 0 + + +def test_pdf_open(tmp_path): + # fail on a buffer full of null bytes + with pytest.raises(PdfParser.PdfFormatError): + PdfParser.PdfParser(buf=bytearray(65536)) + + # make an empty PDF object + with PdfParser.PdfParser() as empty_pdf: + assert len(empty_pdf.pages) == 0 + assert len(empty_pdf.info) == 0 + assert not empty_pdf.should_close_buf + assert not empty_pdf.should_close_file + + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB") + + # open the PDF file + with PdfParser.PdfParser(filename=pdf_filename) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert hopper_pdf.should_close_buf + assert hopper_pdf.should_close_file + + # read a PDF file from a buffer with a non-zero offset + with open(pdf_filename, "rb") as f: + content = b"xyzzy" + f.read() + with PdfParser.PdfParser(buf=content, start_offset=5) as hopper_pdf: + assert len(hopper_pdf.pages) == 1 + assert not hopper_pdf.should_close_buf + assert not hopper_pdf.should_close_file + + # read a PDF file from an already open file + with open(pdf_filename, "rb") as f: + with PdfParser.PdfParser(f=f) as hopper_pdf: assert len(hopper_pdf.pages) == 1 - assert not hopper_pdf.should_close_buf + assert hopper_pdf.should_close_buf assert not hopper_pdf.should_close_file - # read a PDF file from an already open file - with open(pdf_filename, "rb") as f: - with PdfParser.PdfParser(f=f) as hopper_pdf: - assert len(hopper_pdf.pages) == 1 - assert hopper_pdf.should_close_buf - assert not hopper_pdf.should_close_file - - def test_pdf_append_fails_on_nonexistent_file(self): - im = hopper("RGB") - with tempfile.TemporaryDirectory() as temp_dir: - with pytest.raises(IOError): - im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) - - def check_pdf_pages_consistency(self, pdf): - pages_info = pdf.read_indirect(pdf.pages_ref) - assert b"Parent" not in pages_info - assert b"Kids" in pages_info - kids_not_used = pages_info[b"Kids"] - for page_ref in pdf.pages: - while True: - if page_ref in kids_not_used: - kids_not_used.remove(page_ref) - page_info = pdf.read_indirect(page_ref) - assert b"Parent" in page_info - page_ref = page_info[b"Parent"] - if page_ref == pdf.pages_ref: - break - assert pdf.pages_ref == page_info[b"Parent"] - assert kids_not_used == [] - - def test_pdf_append(self): - # make a PDF file - pdf_filename = self.helper_save_as_pdf("RGB", producer="PdfParser") - - # open it, check pages and info - with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: - assert len(pdf.pages) == 1 - assert len(pdf.info) == 4 - assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0] - assert pdf.info.Producer == "PdfParser" - assert b"CreationDate" in pdf.info - assert b"ModDate" in pdf.info - self.check_pdf_pages_consistency(pdf) - - # append some info - pdf.info.Title = "abc" - pdf.info.Author = "def" - pdf.info.Subject = "ghi\uABCD" - pdf.info.Keywords = "qw)e\\r(ty" - pdf.info.Creator = "hopper()" - pdf.start_writing() - pdf.write_xref_and_trailer() - - # open it again, check pages and info again - with PdfParser.PdfParser(pdf_filename) as pdf: - assert len(pdf.pages) == 1 - assert len(pdf.info) == 8 - assert pdf.info.Title == "abc" - assert b"CreationDate" in pdf.info - assert b"ModDate" in pdf.info - self.check_pdf_pages_consistency(pdf) - - # append two images - mode_CMYK = hopper("CMYK") - mode_P = hopper("P") - mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) - - # open the PDF again, check pages and info again - with PdfParser.PdfParser(pdf_filename) as pdf: - assert len(pdf.pages) == 3 - assert len(pdf.info) == 8 - assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc" - assert pdf.info.Title == "abc" - assert pdf.info.Producer == "PdfParser" - assert pdf.info.Keywords == "qw)e\\r(ty" - assert pdf.info.Subject == "ghi\uABCD" - assert b"CreationDate" in pdf.info - assert b"ModDate" in pdf.info - self.check_pdf_pages_consistency(pdf) - - def test_pdf_info(self): - # make a PDF file - pdf_filename = self.helper_save_as_pdf( - "RGB", - title="title", - author="author", - subject="subject", - keywords="keywords", - creator="creator", - producer="producer", - creationDate=time.strptime("2000", "%Y"), - modDate=time.strptime("2001", "%Y"), - ) - - # open it, check pages and info - with PdfParser.PdfParser(pdf_filename) as pdf: - assert len(pdf.info) == 8 - assert pdf.info.Title == "title" - assert pdf.info.Author == "author" - assert pdf.info.Subject == "subject" - assert pdf.info.Keywords == "keywords" - assert pdf.info.Creator == "creator" - assert pdf.info.Producer == "producer" - assert pdf.info.CreationDate == time.strptime("2000", "%Y") - assert pdf.info.ModDate == time.strptime("2001", "%Y") - self.check_pdf_pages_consistency(pdf) - - def test_pdf_append_to_bytesio(self): - im = hopper("RGB") - f = io.BytesIO() - im.save(f, format="PDF") - initial_size = len(f.getvalue()) - assert initial_size > 0 - im = hopper("P") - f = io.BytesIO(f.getvalue()) - im.save(f, format="PDF", append=True) - assert len(f.getvalue()) > initial_size + +def test_pdf_append_fails_on_nonexistent_file(): + im = hopper("RGB") + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(IOError): + im.save(os.path.join(temp_dir, "nonexistent.pdf"), append=True) + + +def check_pdf_pages_consistency(pdf): + pages_info = pdf.read_indirect(pdf.pages_ref) + assert b"Parent" not in pages_info + assert b"Kids" in pages_info + kids_not_used = pages_info[b"Kids"] + for page_ref in pdf.pages: + while True: + if page_ref in kids_not_used: + kids_not_used.remove(page_ref) + page_info = pdf.read_indirect(page_ref) + assert b"Parent" in page_info + page_ref = page_info[b"Parent"] + if page_ref == pdf.pages_ref: + break + assert pdf.pages_ref == page_info[b"Parent"] + assert kids_not_used == [] + + +def test_pdf_append(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf(tmp_path, "RGB", producer="PdfParser") + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename, mode="r+b") as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 4 + assert pdf.info.Title == os.path.splitext(os.path.basename(pdf_filename))[0] + assert pdf.info.Producer == "PdfParser" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append some info + pdf.info.Title = "abc" + pdf.info.Author = "def" + pdf.info.Subject = "ghi\uABCD" + pdf.info.Keywords = "qw)e\\r(ty" + pdf.info.Creator = "hopper()" + pdf.start_writing() + pdf.write_xref_and_trailer() + + # open it again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 1 + assert len(pdf.info) == 8 + assert pdf.info.Title == "abc" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + # append two images + mode_CMYK = hopper("CMYK") + mode_P = hopper("P") + mode_CMYK.save(pdf_filename, append=True, save_all=True, append_images=[mode_P]) + + # open the PDF again, check pages and info again + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.pages) == 3 + assert len(pdf.info) == 8 + assert PdfParser.decode_text(pdf.info[b"Title"]) == "abc" + assert pdf.info.Title == "abc" + assert pdf.info.Producer == "PdfParser" + assert pdf.info.Keywords == "qw)e\\r(ty" + assert pdf.info.Subject == "ghi\uABCD" + assert b"CreationDate" in pdf.info + assert b"ModDate" in pdf.info + check_pdf_pages_consistency(pdf) + + +def test_pdf_info(tmp_path): + # make a PDF file + pdf_filename = helper_save_as_pdf( + tmp_path, + "RGB", + title="title", + author="author", + subject="subject", + keywords="keywords", + creator="creator", + producer="producer", + creationDate=time.strptime("2000", "%Y"), + modDate=time.strptime("2001", "%Y"), + ) + + # open it, check pages and info + with PdfParser.PdfParser(pdf_filename) as pdf: + assert len(pdf.info) == 8 + assert pdf.info.Title == "title" + assert pdf.info.Author == "author" + assert pdf.info.Subject == "subject" + assert pdf.info.Keywords == "keywords" + assert pdf.info.Creator == "creator" + assert pdf.info.Producer == "producer" + assert pdf.info.CreationDate == time.strptime("2000", "%Y") + assert pdf.info.ModDate == time.strptime("2001", "%Y") + check_pdf_pages_consistency(pdf) + + +def test_pdf_append_to_bytesio(): + im = hopper("RGB") + f = io.BytesIO() + im.save(f, format="PDF") + initial_size = len(f.getvalue()) + assert initial_size > 0 + im = hopper("P") + f = io.BytesIO(f.getvalue()) + im.save(f, format="PDF", append=True) + assert len(f.getvalue()) > initial_size diff --git a/Tests/test_file_tga.py b/Tests/test_file_tga.py index 53faa01bd13..4919ad76627 100644 --- a/Tests/test_file_tga.py +++ b/Tests/test_file_tga.py @@ -5,204 +5,202 @@ import pytest from PIL import Image -from .helper import PillowTestCase, assert_image_equal, hopper +from .helper import assert_image_equal, hopper _TGA_DIR = os.path.join("Tests", "images", "tga") _TGA_DIR_COMMON = os.path.join(_TGA_DIR, "common") -class TestFileTga(PillowTestCase): +_MODES = ("L", "LA", "P", "RGB", "RGBA") +_ORIGINS = ("tl", "bl") - _MODES = ("L", "LA", "P", "RGB", "RGBA") - _ORIGINS = ("tl", "bl") +_ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} - _ORIGIN_TO_ORIENTATION = {"tl": 1, "bl": -1} - def test_sanity(self): - for mode in self._MODES: - png_paths = glob( - os.path.join(_TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower())) - ) +def test_sanity(tmp_path): + for mode in _MODES: - for png_path in png_paths: - with Image.open(png_path) as reference_im: - assert reference_im.mode == mode + def roundtrip(original_im): + out = str(tmp_path / "temp.tga") - path_no_ext = os.path.splitext(png_path)[0] - for origin, rle in product(self._ORIGINS, (True, False)): - tga_path = "{}_{}_{}.tga".format( - path_no_ext, origin, "rle" if rle else "raw" - ) + original_im.save(out, rle=rle) + with Image.open(out) as saved_im: + if rle: + assert ( + saved_im.info["compression"] == original_im.info["compression"] + ) + assert saved_im.info["orientation"] == original_im.info["orientation"] + if mode == "P": + assert saved_im.getpalette() == original_im.getpalette() - with Image.open(tga_path) as original_im: - assert original_im.format == "TGA" - assert original_im.get_format_mimetype() == "image/x-tga" - if rle: - assert original_im.info["compression"] == "tga_rle" - assert ( - original_im.info["orientation"] - == self._ORIGIN_TO_ORIENTATION[origin] - ) - if mode == "P": - assert ( - original_im.getpalette() - == reference_im.getpalette() - ) - - assert_image_equal(original_im, reference_im) - - # Generate a new test name every time so the - # test will not fail with permission error - # on Windows. - out = self.tempfile("temp.tga") - - original_im.save(out, rle=rle) - with Image.open(out) as saved_im: - if rle: - assert ( - saved_im.info["compression"] - == original_im.info["compression"] - ) - assert ( - saved_im.info["orientation"] - == original_im.info["orientation"] - ) - if mode == "P": - assert ( - saved_im.getpalette() - == original_im.getpalette() - ) - - assert_image_equal(saved_im, original_im) - - def test_id_field(self): - # tga file with id field - test_file = "Tests/images/tga_id_field.tga" - - # Act - with Image.open(test_file) as im: - - # Assert - assert im.size == (100, 100) - - def test_id_field_rle(self): - # tga file with id field - test_file = "Tests/images/rgb32rle.tga" - - # Act - with Image.open(test_file) as im: - - # Assert - assert im.size == (199, 199) - - def test_save(self): - test_file = "Tests/images/tga_id_field.tga" - with Image.open(test_file) as im: - out = self.tempfile("temp.tga") - - # Save - im.save(out) - with Image.open(out) as test_im: - assert test_im.size == (100, 100) - assert test_im.info["id_section"] == im.info["id_section"] - - # RGBA save - im.convert("RGBA").save(out) - with Image.open(out) as test_im: - assert test_im.size == (100, 100) + assert_image_equal(saved_im, original_im) - def test_save_wrong_mode(self): - im = hopper("PA") - out = self.tempfile("temp.tga") + png_paths = glob( + os.path.join(_TGA_DIR_COMMON, "*x*_{}.png".format(mode.lower())) + ) - with pytest.raises(OSError): - im.save(out) + for png_path in png_paths: + with Image.open(png_path) as reference_im: + assert reference_im.mode == mode - def test_save_id_section(self): - test_file = "Tests/images/rgb32rle.tga" - with Image.open(test_file) as im: - out = self.tempfile("temp.tga") + path_no_ext = os.path.splitext(png_path)[0] + for origin, rle in product(_ORIGINS, (True, False)): + tga_path = "{}_{}_{}.tga".format( + path_no_ext, origin, "rle" if rle else "raw" + ) - # Check there is no id section - im.save(out) - with Image.open(out) as test_im: - assert "id_section" not in test_im.info + with Image.open(tga_path) as original_im: + assert original_im.format == "TGA" + assert original_im.get_format_mimetype() == "image/x-tga" + if rle: + assert original_im.info["compression"] == "tga_rle" + assert ( + original_im.info["orientation"] + == _ORIGIN_TO_ORIENTATION[origin] + ) + if mode == "P": + assert original_im.getpalette() == reference_im.getpalette() - # Save with custom id section - im.save(out, id_section=b"Test content") - with Image.open(out) as test_im: - assert test_im.info["id_section"] == b"Test content" + assert_image_equal(original_im, reference_im) - # Save with custom id section greater than 255 characters - id_section = b"Test content" * 25 - pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) - with Image.open(out) as test_im: - assert test_im.info["id_section"] == id_section[:255] + roundtrip(original_im) - test_file = "Tests/images/tga_id_field.tga" - with Image.open(test_file) as im: - # Save with no id section - im.save(out, id_section="") - with Image.open(out) as test_im: - assert "id_section" not in test_im.info +def test_id_field(): + # tga file with id field + test_file = "Tests/images/tga_id_field.tga" - def test_save_orientation(self): - test_file = "Tests/images/rgb32rle.tga" - out = self.tempfile("temp.tga") - with Image.open(test_file) as im: - assert im.info["orientation"] == -1 + # Act + with Image.open(test_file) as im: - im.save(out, orientation=1) - with Image.open(out) as test_im: - assert test_im.info["orientation"] == 1 + # Assert + assert im.size == (100, 100) - def test_save_rle(self): - test_file = "Tests/images/rgb32rle.tga" - with Image.open(test_file) as im: - assert im.info["compression"] == "tga_rle" - out = self.tempfile("temp.tga") +def test_id_field_rle(): + # tga file with id field + test_file = "Tests/images/rgb32rle.tga" - # Save - im.save(out) - with Image.open(out) as test_im: - assert test_im.size == (199, 199) - assert test_im.info["compression"] == "tga_rle" + # Act + with Image.open(test_file) as im: - # Save without compression - im.save(out, compression=None) + # Assert + assert im.size == (199, 199) + + +def test_save(tmp_path): + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") + + # Save + im.save(out) with Image.open(out) as test_im: - assert "compression" not in test_im.info + assert test_im.size == (100, 100) + assert test_im.info["id_section"] == im.info["id_section"] # RGBA save im.convert("RGBA").save(out) - with Image.open(out) as test_im: - assert test_im.size == (199, 199) + with Image.open(out) as test_im: + assert test_im.size == (100, 100) - test_file = "Tests/images/tga_id_field.tga" - with Image.open(test_file) as im: - assert "compression" not in im.info - # Save with compression - im.save(out, compression="tga_rle") - with Image.open(out) as test_im: - assert test_im.info["compression"] == "tga_rle" +def test_save_wrong_mode(tmp_path): + im = hopper("PA") + out = str(tmp_path / "temp.tga") - def test_save_l_transparency(self): - # There are 559 transparent pixels in la.tga. - num_transparent = 559 + with pytest.raises(OSError): + im.save(out) - in_file = "Tests/images/la.tga" - with Image.open(in_file) as im: - assert im.mode == "LA" - assert im.getchannel("A").getcolors()[0][0] == num_transparent - out = self.tempfile("temp.tga") - im.save(out) +def test_save_id_section(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + out = str(tmp_path / "temp.tga") - with Image.open(out) as test_im: - assert test_im.mode == "LA" - assert test_im.getchannel("A").getcolors()[0][0] == num_transparent + # Check there is no id section + im.save(out) + with Image.open(out) as test_im: + assert "id_section" not in test_im.info + + # Save with custom id section + im.save(out, id_section=b"Test content") + with Image.open(out) as test_im: + assert test_im.info["id_section"] == b"Test content" + + # Save with custom id section greater than 255 characters + id_section = b"Test content" * 25 + pytest.warns(UserWarning, lambda: im.save(out, id_section=id_section)) + with Image.open(out) as test_im: + assert test_im.info["id_section"] == id_section[:255] + + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + + # Save with no id section + im.save(out, id_section="") + with Image.open(out) as test_im: + assert "id_section" not in test_im.info + + +def test_save_orientation(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + out = str(tmp_path / "temp.tga") + with Image.open(test_file) as im: + assert im.info["orientation"] == -1 + + im.save(out, orientation=1) + with Image.open(out) as test_im: + assert test_im.info["orientation"] == 1 + + +def test_save_rle(tmp_path): + test_file = "Tests/images/rgb32rle.tga" + with Image.open(test_file) as im: + assert im.info["compression"] == "tga_rle" + + out = str(tmp_path / "temp.tga") + + # Save + im.save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + assert test_im.info["compression"] == "tga_rle" + + # Save without compression + im.save(out, compression=None) + with Image.open(out) as test_im: + assert "compression" not in test_im.info + + # RGBA save + im.convert("RGBA").save(out) + with Image.open(out) as test_im: + assert test_im.size == (199, 199) + + test_file = "Tests/images/tga_id_field.tga" + with Image.open(test_file) as im: + assert "compression" not in im.info + + # Save with compression + im.save(out, compression="tga_rle") + with Image.open(out) as test_im: + assert test_im.info["compression"] == "tga_rle" + + +def test_save_l_transparency(tmp_path): + # There are 559 transparent pixels in la.tga. + num_transparent = 559 + + in_file = "Tests/images/la.tga" + with Image.open(in_file) as im: + assert im.mode == "LA" + assert im.getchannel("A").getcolors()[0][0] == num_transparent + + out = str(tmp_path / "temp.tga") + im.save(out) + + with Image.open(out) as test_im: + assert test_im.mode == "LA" + assert test_im.getchannel("A").getcolors()[0][0] == num_transparent - assert_image_equal(im, test_im) + assert_image_equal(im, test_im) diff --git a/Tests/test_file_webp.py b/Tests/test_file_webp.py index 5f9176af097..22957f06d9b 100644 --- a/Tests/test_file_webp.py +++ b/Tests/test_file_webp.py @@ -2,7 +2,6 @@ from PIL import Image, WebPImagePlugin from .helper import ( - PillowTestCase, assert_image_similar, assert_image_similar_tofile, hopper, @@ -17,23 +16,21 @@ HAVE_WEBP = False -class TestUnsupportedWebp(PillowTestCase): +class TestUnsupportedWebp: def test_unsupported(self): if HAVE_WEBP: WebPImagePlugin.SUPPORTED = False file_path = "Tests/images/hopper.webp" - pytest.warns( - UserWarning, lambda: self.assertRaises(IOError, Image.open, file_path) - ) + pytest.warns(UserWarning, lambda: pytest.raises(IOError, Image.open, file_path)) if HAVE_WEBP: WebPImagePlugin.SUPPORTED = True @skip_unless_feature("webp") -class TestFileWebp(PillowTestCase): - def setUp(self): +class TestFileWebp: + def setup_method(self): self.rgb_mode = "RGB" def test_version(self): @@ -57,13 +54,13 @@ def test_read_rgb(self): # dwebp -ppm ../../Tests/images/hopper.webp -o hopper_webp_bits.ppm assert_image_similar_tofile(image, "Tests/images/hopper_webp_bits.ppm", 1.0) - def test_write_rgb(self): + def test_write_rgb(self, tmp_path): """ Can we write a RGB mode file to webp without error. Does it have the bits we expect? """ - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") hopper(self.rgb_mode).save(temp_file) with Image.open(temp_file) as image: @@ -86,13 +83,13 @@ def test_write_rgb(self): target = hopper(self.rgb_mode) assert_image_similar(image, target, 12.0) - def test_write_unsupported_mode_L(self): + def test_write_unsupported_mode_L(self, tmp_path): """ Saving a black-and-white file to WebP format should work, and be similar to the original file. """ - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") hopper("L").save(temp_file) with Image.open(temp_file) as image: assert image.mode == self.rgb_mode @@ -105,13 +102,13 @@ def test_write_unsupported_mode_L(self): assert_image_similar(image, target, 10.0) - def test_write_unsupported_mode_P(self): + def test_write_unsupported_mode_P(self, tmp_path): """ Saving a palette-based file to WebP format should work, and be similar to the original file. """ - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") hopper("P").save(temp_file) with Image.open(temp_file) as image: assert image.mode == self.rgb_mode @@ -146,10 +143,10 @@ def test_WebPDecode_with_invalid_args(self): with pytest.raises(TypeError): _webp.WebPDecode() - def test_no_resource_warning(self): + def test_no_resource_warning(self, tmp_path): file_path = "Tests/images/hopper.webp" with Image.open(file_path) as image: - temp_file = self.tempfile("temp.webp") + temp_file = str(tmp_path / "temp.webp") pytest.warns(None, image.save, temp_file) def test_file_pointer_could_be_reused(self): @@ -160,16 +157,16 @@ def test_file_pointer_could_be_reused(self): @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_background_from_gif(self): + def test_background_from_gif(self, tmp_path): with Image.open("Tests/images/chi.gif") as im: original_value = im.convert("RGB").getpixel((1, 1)) # Save as WEBP - out_webp = self.tempfile("temp.webp") + out_webp = str(tmp_path / "temp.webp") im.save(out_webp, save_all=True) # Save as GIF - out_gif = self.tempfile("temp.gif") + out_gif = str(tmp_path / "temp.gif") Image.open(out_webp).save(out_gif) with Image.open(out_gif) as reread: diff --git a/Tests/test_image_reduce.py b/Tests/test_image_reduce.py index b41ae919bae..658a0f5134e 100644 --- a/Tests/test_image_reduce.py +++ b/Tests/test_image_reduce.py @@ -1,238 +1,249 @@ import pytest from PIL import Image, ImageMath, ImageMode -from .helper import PillowTestCase, convert_to_comparable - - -class TestImageReduce(PillowTestCase): - # There are several internal implementations - remarkable_factors = [ - # special implementations - 1, - 2, - 3, - 4, - 5, - 6, - # 1xN implementation - (1, 2), - (1, 3), - (1, 4), - (1, 7), - # Nx1 implementation - (2, 1), - (3, 1), - (4, 1), - (7, 1), - # general implementation with different paths - (4, 6), - (5, 6), - (4, 7), - (5, 7), - (19, 17), - ] - - @classmethod - def setUpClass(cls): - cls.gradients_image = Image.open("Tests/images/radial_gradients.png") - cls.gradients_image.load() - - def test_args_factor(self): - im = Image.new("L", (10, 10)) - - assert (4, 4) == im.reduce(3).size - assert (4, 10) == im.reduce((3, 1)).size - assert (10, 4) == im.reduce((1, 3)).size - - with pytest.raises(ValueError): - im.reduce(0) - with pytest.raises(TypeError): - im.reduce(2.0) - with pytest.raises(ValueError): - im.reduce((0, 10)) - - def test_args_box(self): - im = Image.new("L", (10, 10)) - - assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size - assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size - - with pytest.raises(TypeError): - im.reduce(2, "stri") - with pytest.raises(TypeError): - im.reduce(2, 2) - with pytest.raises(ValueError): - im.reduce(2, (0, 0, 11, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, 0, 10, 11)) - with pytest.raises(ValueError): - im.reduce(2, (-1, 0, 10, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, -1, 10, 10)) - with pytest.raises(ValueError): - im.reduce(2, (0, 5, 10, 5)) - with pytest.raises(ValueError): - im.reduce(2, (5, 0, 5, 10)) - - def test_unsupported_modes(self): - im = Image.new("P", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - - im = Image.new("1", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - - im = Image.new("I;16", (10, 10)) - with pytest.raises(ValueError): - im.reduce(3) - - def get_image(self, mode): - mode_info = ImageMode.getmode(mode) - if mode_info.basetype == "L": - bands = [self.gradients_image] - for _ in mode_info.bands[1:]: - # rotate previous image - band = bands[-1].transpose(Image.ROTATE_90) - bands.append(band) - # Correct alpha channel by transforming completely transparent pixels. - # Low alpha values also emphasize error after alpha multiplication. - if mode.endswith("A"): - bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) - im = Image.merge(mode, bands) - else: - assert len(mode_info.bands) == 1 - im = self.gradients_image.convert(mode) - # change the height to make a not-square image - return im.crop((0, 0, im.width, im.height - 5)) - - def compare_reduce_with_box(self, im, factor): - box = (11, 13, 146, 164) - reduced = im.reduce(factor, box=box) - reference = im.crop(box).reduce(factor) - assert reduced == reference - - def compare_reduce_with_reference(self, im, factor, average_diff=0.4, max_diff=1): - """Image.reduce() should look very similar to Image.resize(BOX). - - A reference image is compiled from a large source area - and possible last column and last row. - +-----------+ - |..........c| - |..........c| - |..........c| - |rrrrrrrrrrp| - +-----------+ - """ - reduced = im.reduce(factor) - - if not isinstance(factor, (list, tuple)): - factor = (factor, factor) - - reference = Image.new(im.mode, reduced.size) - area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) - area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) - area = im.resize(area_size, Image.BOX, area_box) - reference.paste(area, (0, 0)) - - if area_size[0] < reduced.size[0]: - assert reduced.size[0] - area_size[0] == 1 - last_column_box = (area_box[2], 0, im.size[0], area_box[3]) - last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) - reference.paste(last_column, (area_size[0], 0)) - - if area_size[1] < reduced.size[1]: - assert reduced.size[1] - area_size[1] == 1 - last_row_box = (0, area_box[3], area_box[2], im.size[1]) - last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) - reference.paste(last_row, (0, area_size[1])) - - if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: - last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) - last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) - reference.paste(last_pixel, area_size) - - self.assert_compare_images(reduced, reference, average_diff, max_diff) - - def assert_compare_images(self, a, b, max_average_diff, max_diff=255): - assert a.mode == b.mode, "got mode %r, expected %r" % (a.mode, b.mode) - assert a.size == b.size, "got size %r, expected %r" % (a.size, b.size) - - a, b = convert_to_comparable(a, b) - - bands = ImageMode.getmode(a.mode).bands - for band, ach, bch in zip(bands, a.split(), b.split()): - ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch) - ch_hist = ch_diff.histogram() - - average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( - a.size[0] * a.size[1] - ) - msg = "average pixel value difference {:.4f} > expected {:.4f} " - "for '{}' band".format(average_diff, max_average_diff, band) - assert max_average_diff >= average_diff, msg - - last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] - assert ( - max_diff >= last_diff - ), "max pixel value difference {} > expected {} for '{}' band".format( - last_diff, max_diff, band - ) - - def test_mode_L(self): - im = self.get_image("L") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_LA(self): - im = self.get_image("LA") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor, 0.8, 5) - - # With opaque alpha, an error should be way smaller. - im.putalpha(Image.new("L", im.size, 255)) - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_La(self): - im = self.get_image("La") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_RGB(self): - im = self.get_image("RGB") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_RGBA(self): - im = self.get_image("RGBA") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor, 0.8, 5) - - # With opaque alpha, an error should be way smaller. - im.putalpha(Image.new("L", im.size, 255)) - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_RGBa(self): - im = self.get_image("RGBa") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_I(self): - im = self.get_image("I") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor) - self.compare_reduce_with_box(im, factor) - - def test_mode_F(self): - im = self.get_image("F") - for factor in self.remarkable_factors: - self.compare_reduce_with_reference(im, factor, 0, 0) - self.compare_reduce_with_box(im, factor) +from .helper import convert_to_comparable + +# There are several internal implementations +remarkable_factors = [ + # special implementations + 1, + 2, + 3, + 4, + 5, + 6, + # 1xN implementation + (1, 2), + (1, 3), + (1, 4), + (1, 7), + # Nx1 implementation + (2, 1), + (3, 1), + (4, 1), + (7, 1), + # general implementation with different paths + (4, 6), + (5, 6), + (4, 7), + (5, 7), + (19, 17), +] + +gradients_image = Image.open("Tests/images/radial_gradients.png") +gradients_image.load() + + +def test_args_factor(): + im = Image.new("L", (10, 10)) + + assert (4, 4) == im.reduce(3).size + assert (4, 10) == im.reduce((3, 1)).size + assert (10, 4) == im.reduce((1, 3)).size + + with pytest.raises(ValueError): + im.reduce(0) + with pytest.raises(TypeError): + im.reduce(2.0) + with pytest.raises(ValueError): + im.reduce((0, 10)) + + +def test_args_box(): + im = Image.new("L", (10, 10)) + + assert (5, 5) == im.reduce(2, (0, 0, 10, 10)).size + assert (1, 1) == im.reduce(2, (5, 5, 6, 6)).size + + with pytest.raises(TypeError): + im.reduce(2, "stri") + with pytest.raises(TypeError): + im.reduce(2, 2) + with pytest.raises(ValueError): + im.reduce(2, (0, 0, 11, 10)) + with pytest.raises(ValueError): + im.reduce(2, (0, 0, 10, 11)) + with pytest.raises(ValueError): + im.reduce(2, (-1, 0, 10, 10)) + with pytest.raises(ValueError): + im.reduce(2, (0, -1, 10, 10)) + with pytest.raises(ValueError): + im.reduce(2, (0, 5, 10, 5)) + with pytest.raises(ValueError): + im.reduce(2, (5, 0, 5, 10)) + + +def test_unsupported_modes(): + im = Image.new("P", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + im = Image.new("1", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + im = Image.new("I;16", (10, 10)) + with pytest.raises(ValueError): + im.reduce(3) + + +def get_image(mode): + mode_info = ImageMode.getmode(mode) + if mode_info.basetype == "L": + bands = [gradients_image] + for _ in mode_info.bands[1:]: + # rotate previous image + band = bands[-1].transpose(Image.ROTATE_90) + bands.append(band) + # Correct alpha channel by transforming completely transparent pixels. + # Low alpha values also emphasize error after alpha multiplication. + if mode.endswith("A"): + bands[-1] = bands[-1].point(lambda x: int(85 + x / 1.5)) + im = Image.merge(mode, bands) + else: + assert len(mode_info.bands) == 1 + im = gradients_image.convert(mode) + # change the height to make a not-square image + return im.crop((0, 0, im.width, im.height - 5)) + + +def compare_reduce_with_box(im, factor): + box = (11, 13, 146, 164) + reduced = im.reduce(factor, box=box) + reference = im.crop(box).reduce(factor) + assert reduced == reference + + +def compare_reduce_with_reference(im, factor, average_diff=0.4, max_diff=1): + """Image.reduce() should look very similar to Image.resize(BOX). + + A reference image is compiled from a large source area + and possible last column and last row. + +-----------+ + |..........c| + |..........c| + |..........c| + |rrrrrrrrrrp| + +-----------+ + """ + reduced = im.reduce(factor) + + if not isinstance(factor, (list, tuple)): + factor = (factor, factor) + + reference = Image.new(im.mode, reduced.size) + area_size = (im.size[0] // factor[0], im.size[1] // factor[1]) + area_box = (0, 0, area_size[0] * factor[0], area_size[1] * factor[1]) + area = im.resize(area_size, Image.BOX, area_box) + reference.paste(area, (0, 0)) + + if area_size[0] < reduced.size[0]: + assert reduced.size[0] - area_size[0] == 1 + last_column_box = (area_box[2], 0, im.size[0], area_box[3]) + last_column = im.resize((1, area_size[1]), Image.BOX, last_column_box) + reference.paste(last_column, (area_size[0], 0)) + + if area_size[1] < reduced.size[1]: + assert reduced.size[1] - area_size[1] == 1 + last_row_box = (0, area_box[3], area_box[2], im.size[1]) + last_row = im.resize((area_size[0], 1), Image.BOX, last_row_box) + reference.paste(last_row, (0, area_size[1])) + + if area_size[0] < reduced.size[0] and area_size[1] < reduced.size[1]: + last_pixel_box = (area_box[2], area_box[3], im.size[0], im.size[1]) + last_pixel = im.resize((1, 1), Image.BOX, last_pixel_box) + reference.paste(last_pixel, area_size) + + assert_compare_images(reduced, reference, average_diff, max_diff) + + +def assert_compare_images(a, b, max_average_diff, max_diff=255): + assert a.mode == b.mode, "got mode %r, expected %r" % (a.mode, b.mode) + assert a.size == b.size, "got size %r, expected %r" % (a.size, b.size) + + a, b = convert_to_comparable(a, b) + + bands = ImageMode.getmode(a.mode).bands + for band, ach, bch in zip(bands, a.split(), b.split()): + ch_diff = ImageMath.eval("convert(abs(a - b), 'L')", a=ach, b=bch) + ch_hist = ch_diff.histogram() + + average_diff = sum(i * num for i, num in enumerate(ch_hist)) / ( + a.size[0] * a.size[1] + ) + msg = "average pixel value difference {:.4f} > expected {:.4f} " + "for '{}' band".format(average_diff, max_average_diff, band) + assert max_average_diff >= average_diff, msg + + last_diff = [i for i, num in enumerate(ch_hist) if num > 0][-1] + assert ( + max_diff >= last_diff + ), "max pixel value difference {} > expected {} for '{}' band".format( + last_diff, max_diff, band + ) + + +def test_mode_L(): + im = get_image("L") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_LA(): + im = get_image("LA") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor, 0.8, 5) + + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_La(): + im = get_image("La") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_RGB(): + im = get_image("RGB") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_RGBA(): + im = get_image("RGBA") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor, 0.8, 5) + + # With opaque alpha, an error should be way smaller. + im.putalpha(Image.new("L", im.size, 255)) + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_RGBa(): + im = get_image("RGBa") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_I(): + im = get_image("I") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor) + compare_reduce_with_box(im, factor) + + +def test_mode_F(): + im = get_image("F") + for factor in remarkable_factors: + compare_reduce_with_reference(im, factor, 0, 0) + compare_reduce_with_box(im, factor) diff --git a/Tests/test_image_transform.py b/Tests/test_image_transform.py index c833370a510..3409d86f08d 100644 --- a/Tests/test_image_transform.py +++ b/Tests/test_image_transform.py @@ -3,10 +3,10 @@ import pytest from PIL import Image, ImageTransform -from .helper import PillowTestCase, assert_image_equal, assert_image_similar, hopper +from .helper import assert_image_equal, assert_image_similar, hopper -class TestImageTransform(PillowTestCase): +class TestImageTransform: def test_sanity(self): im = Image.new("L", (100, 100)) @@ -177,7 +177,7 @@ def test_unknown_resampling_filter(self): im.transform((100, 100), Image.EXTENT, (0, 0, w, h), resample) -class TestImageTransformAffine(PillowTestCase): +class TestImageTransformAffine: transform = Image.AFFINE def _test_image(self): diff --git a/Tests/test_imagefile.py b/Tests/test_imagefile.py index c3086700a04..883e3f5668e 100644 --- a/Tests/test_imagefile.py +++ b/Tests/test_imagefile.py @@ -4,7 +4,6 @@ from PIL import EpsImagePlugin, Image, ImageFile, features from .helper import ( - PillowTestCase, assert_image, assert_image_equal, assert_image_similar, @@ -19,7 +18,7 @@ SAFEBLOCK = ImageFile.SAFEBLOCK -class TestImageFile(PillowTestCase): +class TestImageFile: def test_parser(self): def roundtrip(format): @@ -163,7 +162,7 @@ def _open(self): self.tile = [("MOCK", (xoff, yoff, xoff + xsize, yoff + ysize), 32, None)] -class TestPyDecoder(PillowTestCase): +class TestPyDecoder: def get_decoder(self): decoder = MockPyDecoder(None) @@ -239,7 +238,7 @@ def test_no_format(self): assert im.format is None assert im.get_format_mimetype() is None - def test_exif_jpeg(self): + def test_exif_jpeg(self, tmp_path): with Image.open("Tests/images/exif-72dpi-int.jpg") as im: # Little endian exif = im.getexif() assert 258 not in exif @@ -247,7 +246,7 @@ def test_exif_jpeg(self): assert exif[40963] == 450 assert exif[11] == "gThumb 3.0.1" - out = self.tempfile("temp.jpg") + out = str(tmp_path / "temp.jpg") exif[258] = 8 del exif[40960] exif[40963] = 455 @@ -267,7 +266,7 @@ def test_exif_jpeg(self): assert exif[40963] == 200 assert exif[305] == "Adobe Photoshop CC 2017 (Macintosh)" - out = self.tempfile("temp.jpg") + out = str(tmp_path / "temp.jpg") exif[258] = 8 del exif[34665] exif[40963] = 455 @@ -282,12 +281,12 @@ def test_exif_jpeg(self): @skip_unless_feature("webp") @skip_unless_feature("webp_anim") - def test_exif_webp(self): + def test_exif_webp(self, tmp_path): with Image.open("Tests/images/hopper.webp") as im: exif = im.getexif() assert exif == {} - out = self.tempfile("temp.webp") + out = str(tmp_path / "temp.webp") exif[258] = 8 exif[40963] = 455 exif[305] = "Pillow test" @@ -304,12 +303,12 @@ def check_exif(): im.save(out, exif=exif, save_all=True) check_exif() - def test_exif_png(self): + def test_exif_png(self, tmp_path): with Image.open("Tests/images/exif.png") as im: exif = im.getexif() assert exif == {274: 1} - out = self.tempfile("temp.png") + out = str(tmp_path / "temp.png") exif[258] = 8 del exif[274] exif[40963] = 455 diff --git a/Tests/test_imagefont.py b/Tests/test_imagefont.py index 690f624d3c0..b3686aea140 100644 --- a/Tests/test_imagefont.py +++ b/Tests/test_imagefont.py @@ -3,7 +3,6 @@ import os import re import shutil -import unittest from io import BytesIO from unittest import mock @@ -441,7 +440,7 @@ def test_unicode_pilfont(self): with pytest.raises(UnicodeEncodeError): font.getsize("’") - @unittest.skipIf(is_pypy(), "failing on PyPy") + @pytest.mark.skipif(is_pypy(), reason="failing on PyPy") def test_unicode_extended(self): # issue #3777 text = "A\u278A\U0001F12B" @@ -478,7 +477,7 @@ def loadable_font(filepath, size, index, encoding, *args, **kwargs): name = font.getname() assert ("FreeMono", "Regular") == name - @unittest.skipIf(is_win32(), "requires Unix or macOS") + @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") def test_find_linux_font(self): # A lot of mocking here - this is more for hitting code and # catching syntax like errors @@ -523,7 +522,7 @@ def fake_walker(path): font_directory + "/Duplicate.ttf", "Duplicate" ) - @unittest.skipIf(is_win32(), "requires Unix or macOS") + @pytest.mark.skipif(is_win32(), reason="requires Unix or macOS") def test_find_macos_font(self): # Like the linux test, more cover hitting code rather than testing # correctness. diff --git a/Tests/test_imagefontctl.py b/Tests/test_imagefontctl.py index de1aa41f318..6d7a9c2f485 100644 --- a/Tests/test_imagefontctl.py +++ b/Tests/test_imagefontctl.py @@ -1,193 +1,207 @@ +import pytest from PIL import Image, ImageDraw, ImageFont -from .helper import PillowTestCase, assert_image_similar, skip_unless_feature +from .helper import assert_image_similar, skip_unless_feature FONT_SIZE = 20 FONT_PATH = "Tests/fonts/DejaVuSans.ttf" +pytestmark = skip_unless_feature("raqm") -@skip_unless_feature("raqm") -class TestImagecomplextext(PillowTestCase): - def test_english(self): - # smoke test, this should not fail - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") - def test_complex_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) +def test_english(): + # smoke test, this should not fail + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TEST", font=ttf, fill=500, direction="ltr") - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) - - target = "Tests/images/test_text.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - def test_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "العالم العربي", font=ttf, fill=500) - - target = "Tests/images/test_y_offset.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 1.7) - - def test_complex_unicode_text(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) - - target = "Tests/images/test_complex_unicode_text.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) - - target = "Tests/images/test_complex_unicode_text2.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 2.3) - - def test_text_direction_rtl(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") - - target = "Tests/images/test_direction_rtl.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - def test_text_direction_ltr(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") - - target = "Tests/images/test_direction_ltr.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - def test_text_direction_rtl2(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") - - target = "Tests/images/test_direction_ltr.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - def test_text_direction_ttb(self): - ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) - - im = Image.new(mode="RGB", size=(100, 300)) - draw = ImageDraw.Draw(im) - try: - draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - self.skipTest("libraqm 0.7 or greater not available") - - target = "Tests/images/test_direction_ttb.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 1.15) - - def test_text_direction_ttb_stroke(self): - ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) - - im = Image.new(mode="RGB", size=(100, 300)) - draw = ImageDraw.Draw(im) - try: - draw.text( - (25, 25), - "あい", - font=ttf, - fill=500, - direction="ttb", - stroke_width=2, - stroke_fill="#0f0", - ) - except ValueError as ex: - if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": - self.skipTest("libraqm 0.7 or greater not available") - - target = "Tests/images/test_direction_ttb_stroke.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 12.4) - - def test_ligature_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) - target = "Tests/images/test_ligature_features.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - liga_size = ttf.getsize("fi", features=["-liga"]) - assert liga_size == (13, 19) - - def test_kerning_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) - - target = "Tests/images/test_kerning_features.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - - def test_arabictext_features(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) + +def test_complex_text(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "اهلا عمان", font=ttf, fill=500) + + target = "Tests/images/test_text.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/NotoNastaliqUrdu-Regular.ttf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "العالم العربي", font=ttf, fill=500) + + target = "Tests/images/test_y_offset.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 1.7) + + +def test_complex_unicode_text(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "السلام عليكم", font=ttf, fill=500) + + target = "Tests/images/test_complex_unicode_text.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + ttf = ImageFont.truetype("Tests/fonts/KhmerOSBattambang-Regular.ttf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "លោកុប្បត្តិ", font=ttf, fill=500) + + target = "Tests/images/test_complex_unicode_text2.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 2.3) + + +def test_text_direction_rtl(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "English عربي", font=ttf, fill=500, direction="rtl") + + target = "Tests/images/test_direction_rtl.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_text_direction_ltr(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "سلطنة عمان Oman", font=ttf, fill=500, direction="ltr") + + target = "Tests/images/test_direction_ltr.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_text_direction_rtl2(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "Oman سلطنة عمان", font=ttf, fill=500, direction="rtl") + + target = "Tests/images/test_direction_ltr.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_text_direction_ttb(): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", FONT_SIZE) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: + draw.text((0, 0), "English あい", font=ttf, fill=500, direction="ttb") + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 1.15) + + +def test_text_direction_ttb_stroke(): + ttf = ImageFont.truetype("Tests/fonts/NotoSansJP-Regular.otf", 50) + + im = Image.new(mode="RGB", size=(100, 300)) + draw = ImageDraw.Draw(im) + try: draw.text( - (0, 0), - "اللغة العربية", + (25, 25), + "あい", font=ttf, fill=500, - features=["-fina", "-init", "-medi"], + direction="ttb", + stroke_width=2, + stroke_fill="#0f0", ) + except ValueError as ex: + if str(ex) == "libraqm 0.7 or greater required for 'ttb' direction": + pytest.skip("libraqm 0.7 or greater not available") + + target = "Tests/images/test_direction_ttb_stroke.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 12.4) + + +def test_ligature_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "filling", font=ttf, fill=500, features=["-liga"]) + target = "Tests/images/test_ligature_features.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + liga_size = ttf.getsize("fi", features=["-liga"]) + assert liga_size == (13, 19) + + +def test_kerning_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "TeToAV", font=ttf, fill=500, features=["-kern"]) + + target = "Tests/images/test_kerning_features.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + + +def test_arabictext_features(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) + + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text( + (0, 0), + "اللغة العربية", + font=ttf, + fill=500, + features=["-fina", "-init", "-medi"], + ) + + target = "Tests/images/test_arabictext_features.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) + - target = "Tests/images/test_arabictext_features.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) +def test_x_max_and_y_offset(): + ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) - def test_x_max_and_y_offset(self): - ttf = ImageFont.truetype("Tests/fonts/ArefRuqaa-Regular.ttf", 40) + im = Image.new(mode="RGB", size=(50, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "لح", font=ttf, fill=500) - im = Image.new(mode="RGB", size=(50, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "لح", font=ttf, fill=500) + target = "Tests/images/test_x_max_and_y_offset.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) - target = "Tests/images/test_x_max_and_y_offset.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) - def test_language(self): - ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) +def test_language(): + ttf = ImageFont.truetype(FONT_PATH, FONT_SIZE) - im = Image.new(mode="RGB", size=(300, 100)) - draw = ImageDraw.Draw(im) - draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") + im = Image.new(mode="RGB", size=(300, 100)) + draw = ImageDraw.Draw(im) + draw.text((0, 0), "абвг", font=ttf, fill=500, language="sr") - target = "Tests/images/test_language.png" - with Image.open(target) as target_img: - assert_image_similar(im, target_img, 0.5) + target = "Tests/images/test_language.png" + with Image.open(target) as target_img: + assert_image_similar(im, target_img, 0.5) diff --git a/Tests/test_imagegrab.py b/Tests/test_imagegrab.py index ffefd1acbd3..9768eb6cec6 100644 --- a/Tests/test_imagegrab.py +++ b/Tests/test_imagegrab.py @@ -1,12 +1,14 @@ import subprocess import sys -from .helper import PillowTestCase, assert_image +import pytest + +from .helper import assert_image try: from PIL import ImageGrab - class TestImageGrab(PillowTestCase): + class TestImageGrab: def test_grab(self): for im in [ ImageGrab.grab(), @@ -39,12 +41,13 @@ def test_grabclipboard(self): except ImportError: - class TestImageGrab(PillowTestCase): + class TestImageGrab: + @pytest.mark.skip(reason="ImageGrab ImportError") def test_skip(self): - self.skipTest("ImportError") + pass -class TestImageGrabImport(PillowTestCase): +class TestImageGrabImport: def test_import(self): # Arrange exception = None diff --git a/Tests/test_imagemorph.py b/Tests/test_imagemorph.py index 5191f99f255..62119e4b3bc 100644 --- a/Tests/test_imagemorph.py +++ b/Tests/test_imagemorph.py @@ -2,318 +2,340 @@ import pytest from PIL import Image, ImageMorph, _imagingmorph -from .helper import PillowTestCase, assert_image_equal, hopper - - -class MorphTests(PillowTestCase): - def setUp(self): - self.A = self.string_to_img( - """ - ....... - ....... - ..111.. - ..111.. - ..111.. - ....... - ....... - """ - ) - - def img_to_string(self, im): - """Turn a (small) binary image into a string representation""" - chars = ".1" - width, height = im.size - return "\n".join( - "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) - for r in range(height) - ) - - def string_to_img(self, image_string): - """Turn a string image representation into a binary image""" - rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] - height = len(rows) - width = len(rows[0]) - im = Image.new("L", (width, height)) - for i in range(width): - for j in range(height): - c = rows[j][i] - v = c in "X1" - im.putpixel((i, j), v) - - return im - - def img_string_normalize(self, im): - return self.img_to_string(self.string_to_img(im)) - - def assert_img_equal(self, A, B): - assert self.img_to_string(A) == self.img_to_string(B) - - def assert_img_equal_img_string(self, A, Bstring): - assert self.img_to_string(A) == self.img_string_normalize(Bstring) - - def test_str_to_img(self): - with Image.open("Tests/images/morph_a.png") as im: - assert_image_equal(self.A, im) - - def create_lut(self): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - lut = lb.build_lut() - with open("Tests/images/%s.lut" % op, "wb") as f: - f.write(lut) - - # create_lut() - def test_lut(self): - for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): - lb = ImageMorph.LutBuilder(op_name=op) - assert lb.get_lut() is None - - lut = lb.build_lut() - with open("Tests/images/%s.lut" % op, "rb") as f: - assert lut == bytearray(f.read()) - - def test_no_operator_loaded(self): - mop = ImageMorph.MorphOp() - with pytest.raises(Exception) as e: - mop.apply(None) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: - mop.match(None) - assert str(e.value) == "No operator loaded" - with pytest.raises(Exception) as e: - mop.save_lut(None) - assert str(e.value) == "No operator loaded" - - # Test the named patterns - def test_erosion8(self): - # erosion8 - mop = ImageMorph.MorphOp(op_name="erosion8") - count, Aout = mop.apply(self.A) - assert count == 8 - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ....... - ...1... - ....... - ....... - ....... - """, - ) - - def test_dialation8(self): - # dialation8 - mop = ImageMorph.MorphOp(op_name="dilation8") - count, Aout = mop.apply(self.A) - assert count == 16 - self.assert_img_equal_img_string( - Aout, - """ - ....... - .11111. - .11111. - .11111. - .11111. - .11111. - ....... - """, - ) - - def test_erosion4(self): - # erosion4 - mop = ImageMorph.MorphOp(op_name="dilation4") - count, Aout = mop.apply(self.A) - assert count == 12 - self.assert_img_equal_img_string( - Aout, - """ - ....... - ..111.. - .11111. - .11111. - .11111. - ..111.. - ....... - """, - ) - - def test_edge(self): - # edge - mop = ImageMorph.MorphOp(op_name="edge") - count, Aout = mop.apply(self.A) - assert count == 1 - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..111.. - ..1.1.. - ..111.. - ....... - ....... - """, - ) - - def test_corner(self): - # Create a corner detector pattern - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) - count, Aout = mop.apply(self.A) - assert count == 5 - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.1.. - ....... - ..1.1.. - ....... - ....... - """, - ) - - # Test the coordinate counting with the same operator - coords = mop.match(self.A) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - - coords = mop.get_on_pixels(Aout) - assert len(coords) == 4 - assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) - - def test_mirroring(self): - # Test 'M' for mirroring - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) - count, Aout = mop.apply(self.A) - assert count == 7 - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.1.. - ....... - ....... - ....... - ....... - """, - ) - - def test_negate(self): - # Test 'N' for negate - mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) - count, Aout = mop.apply(self.A) - assert count == 8 - self.assert_img_equal_img_string( - Aout, - """ - ....... - ....... - ..1.... - ....... - ....... - ....... - ....... - """, - ) - - def test_non_binary_images(self): - im = hopper("RGB") - mop = ImageMorph.MorphOp(op_name="erosion8") - - with pytest.raises(Exception) as e: - mop.apply(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - with pytest.raises(Exception) as e: - mop.match(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - with pytest.raises(Exception) as e: - mop.get_on_pixels(im) - assert str(e.value) == "Image must be binary, meaning it must use mode L" - - def test_add_patterns(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] - new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] - - # Act - lb.add_patterns(new_patterns) - - # Assert - assert lb.patterns == [ - "1:(... ... ...)->0", - "4:(00. 01. ...)->1", - "M:(00. 01. ...)->1", - "N:(00. 01. ...)->1", - ] - - def test_unknown_pattern(self): - with pytest.raises(Exception): - ImageMorph.LutBuilder(op_name="unknown") - - def test_pattern_syntax_error(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") - new_patterns = ["a pattern with a syntax error"] - lb.add_patterns(new_patterns) - - # Act / Assert - with pytest.raises(Exception) as e: - lb.build_lut() - assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' - - def test_load_invalid_mrl(self): - # Arrange - invalid_mrl = "Tests/images/hopper.png" - mop = ImageMorph.MorphOp() - - # Act / Assert - with pytest.raises(Exception) as e: - mop.load_lut(invalid_mrl) - assert str(e.value) == "Wrong size operator file!" - - def test_roundtrip_mrl(self): - # Arrange - tempfile = self.tempfile("temp.mrl") - mop = ImageMorph.MorphOp(op_name="corner") - initial_lut = mop.lut - - # Act - mop.save_lut(tempfile) - mop.load_lut(tempfile) - - # Act / Assert - assert mop.lut == initial_lut - - def test_set_lut(self): - # Arrange - lb = ImageMorph.LutBuilder(op_name="corner") +from .helper import assert_image_equal, hopper + + +def string_to_img(image_string): + """Turn a string image representation into a binary image""" + rows = [s for s in image_string.replace(" ", "").split("\n") if len(s)] + height = len(rows) + width = len(rows[0]) + im = Image.new("L", (width, height)) + for i in range(width): + for j in range(height): + c = rows[j][i] + v = c in "X1" + im.putpixel((i, j), v) + + return im + + +A = string_to_img( + """ + ....... + ....... + ..111.. + ..111.. + ..111.. + ....... + ....... + """ +) + + +def img_to_string(im): + """Turn a (small) binary image into a string representation""" + chars = ".1" + width, height = im.size + return "\n".join( + "".join(chars[im.getpixel((c, r)) > 0] for c in range(width)) + for r in range(height) + ) + + +def img_string_normalize(im): + return img_to_string(string_to_img(im)) + + +def assert_img_equal(A, B): + assert img_to_string(A) == img_to_string(B) + + +def assert_img_equal_img_string(A, Bstring): + assert img_to_string(A) == img_string_normalize(Bstring) + + +def test_str_to_img(): + with Image.open("Tests/images/morph_a.png") as im: + assert_image_equal(A, im) + + +def create_lut(): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): + lb = ImageMorph.LutBuilder(op_name=op) lut = lb.build_lut() - mop = ImageMorph.MorphOp() + with open("Tests/images/%s.lut" % op, "wb") as f: + f.write(lut) + - # Act - mop.set_lut(lut) +# create_lut() +def test_lut(): + for op in ("corner", "dilation4", "dilation8", "erosion4", "erosion8", "edge"): + lb = ImageMorph.LutBuilder(op_name=op) + assert lb.get_lut() is None + + lut = lb.build_lut() + with open("Tests/images/%s.lut" % op, "rb") as f: + assert lut == bytearray(f.read()) + + +def test_no_operator_loaded(): + mop = ImageMorph.MorphOp() + with pytest.raises(Exception) as e: + mop.apply(None) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.match(None) + assert str(e.value) == "No operator loaded" + with pytest.raises(Exception) as e: + mop.save_lut(None) + assert str(e.value) == "No operator loaded" + + +# Test the named patterns +def test_erosion8(): + # erosion8 + mop = ImageMorph.MorphOp(op_name="erosion8") + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ....... + ...1... + ....... + ....... + ....... + """, + ) + + +def test_dialation8(): + # dialation8 + mop = ImageMorph.MorphOp(op_name="dilation8") + count, Aout = mop.apply(A) + assert count == 16 + assert_img_equal_img_string( + Aout, + """ + ....... + .11111. + .11111. + .11111. + .11111. + .11111. + ....... + """, + ) + + +def test_erosion4(): + # erosion4 + mop = ImageMorph.MorphOp(op_name="dilation4") + count, Aout = mop.apply(A) + assert count == 12 + assert_img_equal_img_string( + Aout, + """ + ....... + ..111.. + .11111. + .11111. + .11111. + ..111.. + ....... + """, + ) + + +def test_edge(): + # edge + mop = ImageMorph.MorphOp(op_name="edge") + count, Aout = mop.apply(A) + assert count == 1 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..111.. + ..1.1.. + ..111.. + ....... + ....... + """, + ) + + +def test_corner(): + # Create a corner detector pattern + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "4:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 5 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ..1.1.. + ....... + ....... + """, + ) + + # Test the coordinate counting with the same operator + coords = mop.match(A) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + coords = mop.get_on_pixels(Aout) + assert len(coords) == 4 + assert tuple(coords) == ((2, 2), (4, 2), (2, 4), (4, 4)) + + +def test_mirroring(): + # Test 'M' for mirroring + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "M:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 7 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.1.. + ....... + ....... + ....... + ....... + """, + ) + + +def test_negate(): + # Test 'N' for negate + mop = ImageMorph.MorphOp(patterns=["1:(... ... ...)->0", "N:(00. 01. ...)->1"]) + count, Aout = mop.apply(A) + assert count == 8 + assert_img_equal_img_string( + Aout, + """ + ....... + ....... + ..1.... + ....... + ....... + ....... + ....... + """, + ) + + +def test_non_binary_images(): + im = hopper("RGB") + mop = ImageMorph.MorphOp(op_name="erosion8") + + with pytest.raises(Exception) as e: + mop.apply(im) + assert str(e.value) == "Image must be binary, meaning it must use mode L" + with pytest.raises(Exception) as e: + mop.match(im) + assert str(e.value) == "Image must be binary, meaning it must use mode L" + with pytest.raises(Exception) as e: + mop.get_on_pixels(im) + assert str(e.value) == "Image must be binary, meaning it must use mode L" + + +def test_add_patterns(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + assert lb.patterns == ["1:(... ... ...)->0", "4:(00. 01. ...)->1"] + new_patterns = ["M:(00. 01. ...)->1", "N:(00. 01. ...)->1"] + + # Act + lb.add_patterns(new_patterns) + + # Assert + assert lb.patterns == [ + "1:(... ... ...)->0", + "4:(00. 01. ...)->1", + "M:(00. 01. ...)->1", + "N:(00. 01. ...)->1", + ] + + +def test_unknown_pattern(): + with pytest.raises(Exception): + ImageMorph.LutBuilder(op_name="unknown") + + +def test_pattern_syntax_error(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + new_patterns = ["a pattern with a syntax error"] + lb.add_patterns(new_patterns) + + # Act / Assert + with pytest.raises(Exception) as e: + lb.build_lut() + assert str(e.value) == 'Syntax error in pattern "a pattern with a syntax error"' + + +def test_load_invalid_mrl(): + # Arrange + invalid_mrl = "Tests/images/hopper.png" + mop = ImageMorph.MorphOp() + + # Act / Assert + with pytest.raises(Exception) as e: + mop.load_lut(invalid_mrl) + assert str(e.value) == "Wrong size operator file!" + + +def test_roundtrip_mrl(tmp_path): + # Arrange + tempfile = str(tmp_path / "temp.mrl") + mop = ImageMorph.MorphOp(op_name="corner") + initial_lut = mop.lut + + # Act + mop.save_lut(tempfile) + mop.load_lut(tempfile) + + # Act / Assert + assert mop.lut == initial_lut + + +def test_set_lut(): + # Arrange + lb = ImageMorph.LutBuilder(op_name="corner") + lut = lb.build_lut() + mop = ImageMorph.MorphOp() + + # Act + mop.set_lut(lut) + + # Assert + assert mop.lut == lut - # Assert - assert mop.lut == lut - def test_wrong_mode(self): - lut = ImageMorph.LutBuilder(op_name="corner").build_lut() - imrgb = Image.new("RGB", (10, 10)) - iml = Image.new("L", (10, 10)) +def test_wrong_mode(): + lut = ImageMorph.LutBuilder(op_name="corner").build_lut() + imrgb = Image.new("RGB", (10, 10)) + iml = Image.new("L", (10, 10)) - with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), imrgb.im.id, iml.im.id) - with pytest.raises(RuntimeError): - _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) + with pytest.raises(RuntimeError): + _imagingmorph.apply(bytes(lut), iml.im.id, imrgb.im.id) - with pytest.raises(RuntimeError): - _imagingmorph.match(bytes(lut), imrgb.im.id) + with pytest.raises(RuntimeError): + _imagingmorph.match(bytes(lut), imrgb.im.id) - # Should not raise - _imagingmorph.match(bytes(lut), iml.im.id) + # Should not raise + _imagingmorph.match(bytes(lut), iml.im.id) diff --git a/Tests/test_imagepath.py b/Tests/test_imagepath.py index 62683658d06..52af164551f 100644 --- a/Tests/test_imagepath.py +++ b/Tests/test_imagepath.py @@ -4,10 +4,8 @@ import pytest from PIL import Image, ImagePath -from .helper import PillowTestCase - -class TestImagePath(PillowTestCase): +class TestImagePath: def test_path(self): p = ImagePath.Path(list(range(10))) diff --git a/Tests/test_lib_pack.py b/Tests/test_lib_pack.py index d751766013e..8e3c1fda925 100644 --- a/Tests/test_lib_pack.py +++ b/Tests/test_lib_pack.py @@ -3,12 +3,10 @@ import pytest from PIL import Image -from .helper import PillowTestCase - X = 255 -class TestLibPack(PillowTestCase): +class TestLibPack: def assert_pack(self, mode, rawmode, data, *pixels): """ data - either raw bytes with data or just number of bytes in rawmode. @@ -223,7 +221,7 @@ def test_F_float(self): ) -class TestLibUnpack(PillowTestCase): +class TestLibUnpack: def assert_unpack(self, mode, rawmode, data, *pixels): """ data - either raw bytes with data or just number of bytes in rawmode. diff --git a/Tests/test_mode_i16.py b/Tests/test_mode_i16.py index c0ffb3e091d..19e16f2c44b 100644 --- a/Tests/test_mode_i16.py +++ b/Tests/test_mode_i16.py @@ -1,105 +1,106 @@ from PIL import Image -from .helper import PillowTestCase, hopper +from .helper import hopper +original = hopper().resize((32, 32)).convert("I") -class TestModeI16(PillowTestCase): - original = hopper().resize((32, 32)).convert("I") +def verify(im1): + im2 = original.copy() + assert im1.size == im2.size + pix1 = im1.load() + pix2 = im2.load() + for y in range(im1.size[1]): + for x in range(im1.size[0]): + xy = x, y + p1 = pix1[xy] + p2 = pix2[xy] + assert p1 == p2, "got {!r} from mode {} at {}, expected {!r}".format( + p1, im1.mode, xy, p2 + ) - def verify(self, im1): - im2 = self.original.copy() - assert im1.size == im2.size - pix1 = im1.load() - pix2 = im2.load() - for y in range(im1.size[1]): - for x in range(im1.size[0]): - xy = x, y - p1 = pix1[xy] - p2 = pix2[xy] - assert p1 == p2, "got {!r} from mode {} at {}, expected {!r}".format( - p1, im1.mode, xy, p2 - ) - def test_basic(self): - # PIL 1.1 has limited support for 16-bit image data. Check that - # create/copy/transform and save works as expected. +def test_basic(tmp_path): + # PIL 1.1 has limited support for 16-bit image data. Check that + # create/copy/transform and save works as expected. - def basic(mode): + def basic(mode): - imIn = self.original.convert(mode) - self.verify(imIn) + imIn = original.convert(mode) + verify(imIn) - w, h = imIn.size + w, h = imIn.size - imOut = imIn.copy() - self.verify(imOut) # copy + imOut = imIn.copy() + verify(imOut) # copy - imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) - self.verify(imOut) # transform + imOut = imIn.transform((w, h), Image.EXTENT, (0, 0, w, h)) + verify(imOut) # transform - filename = self.tempfile("temp.im") - imIn.save(filename) + filename = str(tmp_path / "temp.im") + imIn.save(filename) - with Image.open(filename) as imOut: + with Image.open(filename) as imOut: - self.verify(imIn) - self.verify(imOut) + verify(imIn) + verify(imOut) - imOut = imIn.crop((0, 0, w, h)) - self.verify(imOut) + imOut = imIn.crop((0, 0, w, h)) + verify(imOut) - imOut = Image.new(mode, (w, h), None) - imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) - imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) + imOut = Image.new(mode, (w, h), None) + imOut.paste(imIn.crop((0, 0, w // 2, h)), (0, 0)) + imOut.paste(imIn.crop((w // 2, 0, w, h)), (w // 2, 0)) - self.verify(imIn) - self.verify(imOut) + verify(imIn) + verify(imOut) - imIn = Image.new(mode, (1, 1), 1) - assert imIn.getpixel((0, 0)) == 1 + imIn = Image.new(mode, (1, 1), 1) + assert imIn.getpixel((0, 0)) == 1 - imIn.putpixel((0, 0), 2) - assert imIn.getpixel((0, 0)) == 2 + imIn.putpixel((0, 0), 2) + assert imIn.getpixel((0, 0)) == 2 - if mode == "L": - maximum = 255 - else: - maximum = 32767 + if mode == "L": + maximum = 255 + else: + maximum = 32767 - imIn = Image.new(mode, (1, 1), 256) - assert imIn.getpixel((0, 0)) == min(256, maximum) + imIn = Image.new(mode, (1, 1), 256) + assert imIn.getpixel((0, 0)) == min(256, maximum) - imIn.putpixel((0, 0), 512) - assert imIn.getpixel((0, 0)) == min(512, maximum) + imIn.putpixel((0, 0), 512) + assert imIn.getpixel((0, 0)) == min(512, maximum) - basic("L") + basic("L") - basic("I;16") - basic("I;16B") - basic("I;16L") + basic("I;16") + basic("I;16B") + basic("I;16L") - basic("I") + basic("I") - def test_tobytes(self): - def tobytes(mode): - return Image.new(mode, (1, 1), 1).tobytes() - order = 1 if Image._ENDIAN == "<" else -1 +def test_tobytes(): + def tobytes(mode): + return Image.new(mode, (1, 1), 1).tobytes() - assert tobytes("L") == b"\x01" - assert tobytes("I;16") == b"\x01\x00" - assert tobytes("I;16B") == b"\x00\x01" - assert tobytes("I") == b"\x01\x00\x00\x00"[::order] + order = 1 if Image._ENDIAN == "<" else -1 - def test_convert(self): + assert tobytes("L") == b"\x01" + assert tobytes("I;16") == b"\x01\x00" + assert tobytes("I;16B") == b"\x00\x01" + assert tobytes("I") == b"\x01\x00\x00\x00"[::order] - im = self.original.copy() - self.verify(im.convert("I;16")) - self.verify(im.convert("I;16").convert("L")) - self.verify(im.convert("I;16").convert("I")) +def test_convert(): - self.verify(im.convert("I;16B")) - self.verify(im.convert("I;16B").convert("L")) - self.verify(im.convert("I;16B").convert("I")) + im = original.copy() + + verify(im.convert("I;16")) + verify(im.convert("I;16").convert("L")) + verify(im.convert("I;16").convert("I")) + + verify(im.convert("I;16B")) + verify(im.convert("I;16B").convert("L")) + verify(im.convert("I;16B").convert("I")) diff --git a/Tests/test_pickle.py b/Tests/test_pickle.py index b95544d0fbf..ba48689883c 100644 --- a/Tests/test_pickle.py +++ b/Tests/test_pickle.py @@ -2,88 +2,89 @@ from PIL import Image -from .helper import PillowTestCase - - -class TestPickle(PillowTestCase): - def helper_pickle_file(self, pickle, protocol=0, mode=None): - # Arrange - with Image.open("Tests/images/hopper.jpg") as im: - filename = self.tempfile("temp.pkl") - if mode: - im = im.convert(mode) - - # Act - with open(filename, "wb") as f: - pickle.dump(im, f, protocol) - with open(filename, "rb") as f: - loaded_im = pickle.load(f) - - # Assert - assert im == loaded_im - - def helper_pickle_string( - self, pickle, protocol=0, test_file="Tests/images/hopper.jpg", mode=None - ): - with Image.open(test_file) as im: - if mode: - im = im.convert(mode) - - # Act - dumped_string = pickle.dumps(im, protocol) - loaded_im = pickle.loads(dumped_string) - - # Assert - assert im == loaded_im - - def test_pickle_image(self): - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol) - self.helper_pickle_file(pickle, protocol) - - def test_pickle_p_mode(self): - # Act / Assert - for test_file in [ - "Tests/images/test-card.png", - "Tests/images/zero_bb.png", - "Tests/images/zero_bb_scale2.png", - "Tests/images/non_zero_bb.png", - "Tests/images/non_zero_bb_scale2.png", - "Tests/images/p_trns_single.png", - "Tests/images/pil123p.png", - "Tests/images/itxt_chunks.png", - ]: - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string( - pickle, protocol=protocol, test_file=test_file - ) - - def test_pickle_pa_mode(self): - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol, mode="PA") - self.helper_pickle_file(pickle, protocol, mode="PA") - def test_pickle_l_mode(self): - # Act / Assert +def helper_pickle_file(tmp_path, pickle, protocol=0, mode=None): + # Arrange + with Image.open("Tests/images/hopper.jpg") as im: + filename = str(tmp_path / "temp.pkl") + if mode: + im = im.convert(mode) + + # Act + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) + + # Assert + assert im == loaded_im + + +def helper_pickle_string( + pickle, protocol=0, test_file="Tests/images/hopper.jpg", mode=None +): + with Image.open(test_file) as im: + if mode: + im = im.convert(mode) + + # Act + dumped_string = pickle.dumps(im, protocol) + loaded_im = pickle.loads(dumped_string) + + # Assert + assert im == loaded_im + + +def test_pickle_image(tmp_path): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol) + helper_pickle_file(tmp_path, pickle, protocol) + + +def test_pickle_p_mode(): + # Act / Assert + for test_file in [ + "Tests/images/test-card.png", + "Tests/images/zero_bb.png", + "Tests/images/zero_bb_scale2.png", + "Tests/images/non_zero_bb.png", + "Tests/images/non_zero_bb_scale2.png", + "Tests/images/p_trns_single.png", + "Tests/images/pil123p.png", + "Tests/images/itxt_chunks.png", + ]: for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - self.helper_pickle_string(pickle, protocol, mode="L") - self.helper_pickle_file(pickle, protocol, mode="L") + helper_pickle_string(pickle, protocol=protocol, test_file=test_file) - def test_pickle_la_mode_with_palette(self): - # Arrange - filename = self.tempfile("temp.pkl") - with Image.open("Tests/images/hopper.jpg") as im: - im = im.convert("PA") - # Act / Assert - for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): - im.mode = "LA" - with open(filename, "wb") as f: - pickle.dump(im, f, protocol) - with open(filename, "rb") as f: - loaded_im = pickle.load(f) - - im.mode = "PA" - assert im == loaded_im +def test_pickle_pa_mode(tmp_path): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol, mode="PA") + helper_pickle_file(tmp_path, pickle, protocol, mode="PA") + + +def test_pickle_l_mode(tmp_path): + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + helper_pickle_string(pickle, protocol, mode="L") + helper_pickle_file(tmp_path, pickle, protocol, mode="L") + + +def test_pickle_la_mode_with_palette(tmp_path): + # Arrange + filename = str(tmp_path / "temp.pkl") + with Image.open("Tests/images/hopper.jpg") as im: + im = im.convert("PA") + + # Act / Assert + for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): + im.mode = "LA" + with open(filename, "wb") as f: + pickle.dump(im, f, protocol) + with open(filename, "rb") as f: + loaded_im = pickle.load(f) + + im.mode = "PA" + assert im == loaded_im