diff --git a/beets/art.py b/beets/art.py index 2ff58c309e..826910372e 100644 --- a/beets/art.py +++ b/beets/art.py @@ -148,8 +148,8 @@ def resize_image(log, imagepath, maxwidth, quality): maxwidth, quality, ) - imagepath = ArtResizer.shared.resize( - maxwidth, syspath(imagepath), quality=quality + imagepath = ArtResizer.shared.convert( + syspath(imagepath), maxwidth=maxwidth, quality=quality ) return imagepath diff --git a/beets/util/artresizer.py b/beets/util/artresizer.py index 09cc29e0df..9426415027 100644 --- a/beets/util/artresizer.py +++ b/beets/util/artresizer.py @@ -123,56 +123,41 @@ def __init__(self): self.identify_cmd = ["magick", "identify"] self.compare_cmd = ["magick", "compare"] - def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 + def convert( + self, + source, + target=None, + maxwidth=0, + quality=0, + max_filesize=0, + deinterlaced=None, ): - """Resize using ImageMagick. + if not target: + target = get_temp_filename(__name__, "convert_IM_", source) - Use the ``magick`` program or ``convert`` on older versions. Return - the output path of resized image. - """ - if not path_out: - path_out = get_temp_filename(__name__, "resize_IM_", path_in) - - log.debug( - "artresizer: ImageMagick resizing {0} to {1}", - displayable_path(path_in), - displayable_path(path_out), - ) - - # "-resize WIDTHx>" shrinks images with the width larger - # than the given width while maintaining the aspect ratio - # with regards to the height. - # ImageMagick already seems to default to no interlace, but we include - # it here for the sake of explicitness. cmd = self.convert_cmd + [ - syspath(path_in, prefix=False), - "-resize", - f"{maxwidth}x>", - "-interlace", - "none", + syspath(source, prefix=False), + *(["-resize", f"{maxwidth}x>"] if maxwidth > 0 else []), + *(["-quality", f"{quality}"] if quality > 0 else []), + *( + ["-define", f"jpeg:extent={max_filesize}b"] + if max_filesize > 0 + else [] + ), + *(["-interlace", "none"] if deinterlaced else []), + syspath(target, prefix=False), ] - if quality > 0: - cmd += ["-quality", f"{quality}"] - - # "-define jpeg:extent=SIZEb" sets the target filesize for imagemagick - # to SIZE in bytes. - if max_filesize > 0: - cmd += ["-define", f"jpeg:extent={max_filesize}b"] - - cmd.append(syspath(path_out, prefix=False)) - try: util.command_output(cmd) except subprocess.CalledProcessError: log.warning( "artresizer: IM convert failed for {0}", - displayable_path(path_in), + displayable_path(source), ) - return path_in + return source - return path_out + return target def get_size(self, path_in): cmd = self.identify_cmd + [ @@ -199,24 +184,6 @@ def get_size(self, path_in): log.warning("Could not understand IM output: {0!r}", out) return None - def deinterlace(self, path_in, path_out=None): - if not path_out: - path_out = get_temp_filename(__name__, "deinterlace_IM_", path_in) - - cmd = self.convert_cmd + [ - syspath(path_in, prefix=False), - "-interlace", - "none", - syspath(path_out, prefix=False), - ] - - try: - util.command_output(cmd) - return path_out - except subprocess.CalledProcessError: - # FIXME: Should probably issue a warning? - return path_in - def get_format(self, filepath): cmd = self.identify_cmd + ["-format", "%[magick]", syspath(filepath)] @@ -226,22 +193,6 @@ def get_format(self, filepath): # FIXME: Should probably issue a warning? return None - def convert_format(self, source, target, deinterlaced): - cmd = self.convert_cmd + [ - syspath(source), - *(["-interlace", "none"] if deinterlaced else []), - syspath(target), - ] - - try: - subprocess.check_call( - cmd, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL - ) - return target - except subprocess.CalledProcessError: - # FIXME: Should probably issue a warning? - return source - @property def can_compare(self): return self.version() > (6, 8, 7) @@ -353,35 +304,49 @@ def __init__(self): """ self.version() - def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 + def convert( + self, + source, + target=None, + maxwidth=0, + quality=0, + max_filesize=0, + deinterlaced=None, ): - """Resize using Python Imaging Library (PIL). Return the output path - of resized image. + """Adjust image using Python Imaging Library (PIL). Return the output path + of adjusted image. """ - if not path_out: - path_out = get_temp_filename(__name__, "resize_PIL_", path_in) + if not target: + target = get_temp_filename(__name__, "convert_PIL_", source) from PIL import Image - log.debug( - "artresizer: PIL resizing {0} to {1}", - displayable_path(path_in), - displayable_path(path_out), - ) - try: - im = Image.open(syspath(path_in)) - size = maxwidth, maxwidth - im.thumbnail(size, Image.Resampling.LANCZOS) + im = Image.open(syspath(source)) + + if maxwidth > 0: + size = maxwidth, maxwidth + im.thumbnail(size, Image.Resampling.LANCZOS) if quality == 0: # Use PIL's default quality. quality = -1 - # progressive=False only affects JPEGs and is the default, - # but we include it here for explicitness. - im.save(os.fsdecode(path_out), quality=quality, progressive=False) + progressive = None + + if deinterlaced: + progressive = False + elif deinterlaced is False: + progressive = True + + if progressive is not None: + im.save( + os.fsdecode(target), + quality=quality, + progressive=progressive, + ) + else: + im.save(os.fsdecode(target), quality=quality) if max_filesize > 0: # If maximum filesize is set, we attempt to lower the quality @@ -391,38 +356,40 @@ def resize( lower_qual = quality else: lower_qual = 95 + for i in range(5): # 5 attempts is an arbitrary choice - filesize = os.stat(syspath(path_out)).st_size + filesize = os.stat(syspath(target)).st_size log.debug("PIL Pass {0} : Output size: {1}B", i, filesize) if filesize <= max_filesize: - return path_out + return target # The relationship between filesize & quality will be # image dependent. lower_qual -= 10 # Restrict quality dropping below 10 if lower_qual < 10: lower_qual = 10 + # Use optimize flag to improve filesize decrease im.save( - os.fsdecode(path_out), + os.fsdecode(target), quality=lower_qual, optimize=True, - progressive=False, + progressive=progressive, ) log.warning( "PIL Failed to resize file to below {0}B", max_filesize ) - return path_out + return target else: - return path_out + return target except OSError: log.error( - "PIL cannot create thumbnail for '{0}'", - displayable_path(path_in), + "PIL cannot make adjustments to '{0}'", + displayable_path(source), ) - return path_in + return source def get_size(self, path_in): from PIL import Image @@ -436,20 +403,6 @@ def get_size(self, path_in): ) return None - def deinterlace(self, path_in, path_out=None): - if not path_out: - path_out = get_temp_filename(__name__, "deinterlace_PIL_", path_in) - - from PIL import Image - - try: - im = Image.open(syspath(path_in)) - im.save(os.fsdecode(path_out), progressive=False) - return path_out - except OSError: - # FIXME: Should probably issue a warning? - return path_in - def get_format(self, filepath): from PIL import Image, UnidentifiedImageError @@ -465,23 +418,6 @@ def get_format(self, filepath): log.exception("failed to detect image format for {}", filepath) return None - def convert_format(self, source, target, deinterlaced): - from PIL import Image, UnidentifiedImageError - - try: - with Image.open(syspath(source)) as im: - im.save(os.fsdecode(target), progressive=not deinterlaced) - return target - except ( - ValueError, - TypeError, - UnidentifiedImageError, - FileNotFoundError, - OSError, - ): - log.exception("failed to convert image {} -> {}", source, target) - return source - @property def can_compare(self): return False @@ -555,36 +491,44 @@ def method(self): else: return "WEBPROXY" - def resize( - self, maxwidth, path_in, path_out=None, quality=0, max_filesize=0 - ): - """Manipulate an image file according to the method, returning a - new path. For PIL or IMAGEMAGIC methods, resizes the image to a - temporary file and encodes with the specified quality level. - For WEBPROXY, returns `path_in` unmodified. - """ - if self.local: - return self.local_method.resize( - maxwidth, - path_in, - path_out, - quality=quality, - max_filesize=max_filesize, - ) - else: - # Handled by `proxy_url` already. - return path_in + def convert(self, source, **kwargs): + if not self.local: + # FIXME: Should probably issue a warning? + return source - def deinterlace(self, path_in, path_out=None): - """Deinterlace an image. + params = {} - Only available locally. - """ - if self.local: - return self.local_method.deinterlace(path_in, path_out) - else: - # FIXME: Should probably issue a warning? - return path_in + if "new_format" in kwargs: + new_format = kwargs["new_format"].lower() + # A nonexhaustive map of image "types" to extensions overrides + new_format = { + "jpeg": "jpg", + }.get(new_format, new_format) + + fname, ext = os.path.splitext(source) + + target = fname + b"." + new_format.encode("utf8") + params["target"] = target + + if "maxwidth" in kwargs and kwargs["maxwidth"] > 0: + params["maxwidth"] = kwargs["maxwidth"] + + if "quality" in kwargs and kwargs["quality"] > 0: + params["quality"] = kwargs["quality"] + + if "max_filesize" in kwargs and kwargs["max_filesize"] > 0: + params["max_filesize"] = kwargs["max_filesize"] + + if "deinterlaced" in kwargs: + params["deinterlaced"] = kwargs["deinterlaced"] + + result_path = source + try: + result_path = self.local_method.convert(source, **params) + finally: + if result_path != source: + os.unlink(source) + return result_path def proxy_url(self, maxwidth, url, quality=0): """Modifies an image URL according the method, returning a new @@ -627,37 +571,6 @@ def get_format(self, path_in): # FIXME: Should probably issue a warning? return None - def reformat(self, path_in, new_format, deinterlaced=True): - """Converts image to desired format, updating its extension, but - keeping the same filename. - - Only available locally. - """ - if not self.local: - # FIXME: Should probably issue a warning? - return path_in - - new_format = new_format.lower() - # A nonexhaustive map of image "types" to extensions overrides - new_format = { - "jpeg": "jpg", - }.get(new_format, new_format) - - fname, ext = os.path.splitext(path_in) - path_new = fname + b"." + new_format.encode("utf8") - - # allows the exception to propagate, while still making sure a changed - # file path was removed - result_path = path_in - try: - result_path = self.local_method.convert_format( - path_in, path_new, deinterlaced - ) - finally: - if result_path != path_in: - os.unlink(path_in) - return result_path - @property def can_compare(self): """A boolean indicating whether image comparison is available""" diff --git a/beetsplug/fetchart.py b/beetsplug/fetchart.py index 0da884278b..ed741f4846 100644 --- a/beetsplug/fetchart.py +++ b/beetsplug/fetchart.py @@ -44,13 +44,6 @@ class Candidate: dimension restrictions and resizing. """ - CANDIDATE_BAD = 0 - CANDIDATE_EXACT = 1 - CANDIDATE_DOWNSCALE = 2 - CANDIDATE_DOWNSIZE = 3 - CANDIDATE_DEINTERLACE = 4 - CANDIDATE_REFORMAT = 5 - MATCH_EXACT = 0 MATCH_FALLBACK = 1 @@ -65,6 +58,22 @@ def __init__( self.match = match self.size = size + def should_downscale(self, plugin): + return plugin.maxwidth and self.size[0] > plugin.maxwidth + + def should_downsize(self, plugin, filesize): + if plugin.max_filesize: + return filesize > plugin.max_filesize + return False + + def should_reformat(self, plugin, fmt): + if plugin.cover_format: + return fmt != plugin.cover_format + return False + + def should_deinterlace(self, plugin): + return plugin.deinterlace + def _validate(self, plugin, skip_check_for=None): """Determine whether the candidate artwork is valid based on its dimensions (width and ratio). @@ -74,16 +83,11 @@ def _validate(self, plugin, skip_check_for=None): validated for a particular operation without changing plugin configuration. - Return `CANDIDATE_BAD` if the file is unusable. - Return `CANDIDATE_EXACT` if the file is usable as-is. - Return `CANDIDATE_DOWNSCALE` if the file must be rescaled. - Return `CANDIDATE_DOWNSIZE` if the file must be resized, and possibly - also rescaled. - Return `CANDIDATE_DEINTERLACE` if the file must be deinterlaced. - Return `CANDIDATE_REFORMAT` if the file has to be converted. + Return False if the file is unusable. + Otherwise, return True. """ if not self.path: - return self.CANDIDATE_BAD + return False if skip_check_for is None: skip_check_for = [] @@ -98,7 +102,7 @@ def _validate(self, plugin, skip_check_for=None): or plugin.deinterlace or plugin.cover_format ): - return self.CANDIDATE_EXACT + return True # get_size returns None if no local imaging backend is available if not self.size: @@ -113,7 +117,7 @@ def _validate(self, plugin, skip_check_for=None): "`enforce_ratio` and `max_filesize` " "may be violated." ) - return self.CANDIDATE_EXACT + return True short_edge = min(self.size) long_edge = max(self.size) @@ -123,7 +127,7 @@ def _validate(self, plugin, skip_check_for=None): self._log.debug( "image too small ({} < {})", self.size[0], plugin.minwidth ) - return self.CANDIDATE_BAD + return False # Check aspect ratio. edge_diff = long_edge - short_edge @@ -137,7 +141,7 @@ def _validate(self, plugin, skip_check_for=None): short_edge, plugin.margin_px, ) - return self.CANDIDATE_BAD + return False elif plugin.margin_percent: margin_px = plugin.margin_percent * long_edge if edge_diff > margin_px: @@ -148,106 +152,58 @@ def _validate(self, plugin, skip_check_for=None): short_edge, margin_px, ) - return self.CANDIDATE_BAD + return False elif edge_diff: # also reached for margin_px == 0 and margin_percent == 0.0 self._log.debug( "image is not square ({} != {})", self.size[0], self.size[1] ) - return self.CANDIDATE_BAD - - # Check maximum dimension. - downscale = False - if plugin.maxwidth and self.size[0] > plugin.maxwidth: - self._log.debug( - "image needs rescaling ({} > {})", self.size[0], plugin.maxwidth - ) - downscale = True - - # Check filesize. - downsize = False - if plugin.max_filesize: - filesize = os.stat(syspath(self.path)).st_size - if filesize > plugin.max_filesize: - self._log.debug( - "image needs resizing ({}B > {}B)", - filesize, - plugin.max_filesize, - ) - downsize = True + return False - # Check image format - reformat = False - if plugin.cover_format: - fmt = ArtResizer.shared.get_format(self.path) - reformat = fmt != plugin.cover_format - if reformat: - self._log.debug( - "image needs reformatting: {} -> {}", - fmt, - plugin.cover_format, - ) - - if downscale and (self.CANDIDATE_DOWNSCALE not in skip_check_for): - return self.CANDIDATE_DOWNSCALE - if reformat and (self.CANDIDATE_REFORMAT not in skip_check_for): - return self.CANDIDATE_REFORMAT - if plugin.deinterlace and ( - self.CANDIDATE_DEINTERLACE not in skip_check_for - ): - return self.CANDIDATE_DEINTERLACE - if downsize and (self.CANDIDATE_DOWNSIZE not in skip_check_for): - return self.CANDIDATE_DOWNSIZE - return self.CANDIDATE_EXACT + return True def validate(self, plugin, skip_check_for=None): self.check = self._validate(plugin, skip_check_for) return self.check - def resize(self, plugin): - """Resize the candidate artwork according to the plugin's - configuration until it is valid or no further resizing is - possible. + def convert(self, plugin): + """Runs the ArtResizer's convert command on the candidate + if the maxwidth, max_filesize, cover_format, or deinterlace + options are passed in and valid. FetchArtPlugin handles + validation. """ - # validate the candidate in case it hasn't been done yet - current_check = self.validate(plugin) - checks_performed = [] - - # we don't want to resize the image if it's valid or bad - while current_check not in [self.CANDIDATE_BAD, self.CANDIDATE_EXACT]: - self._resize(plugin, current_check) - checks_performed.append(current_check) - current_check = self.validate( - plugin, skip_check_for=checks_performed - ) + convert_params = {} - def _resize(self, plugin, check=None): - """Resize the candidate artwork according to the plugin's - configuration and the specified check. - """ - if check == self.CANDIDATE_DOWNSCALE: - self.path = ArtResizer.shared.resize( - plugin.maxwidth, - self.path, - quality=plugin.quality, - max_filesize=plugin.max_filesize, + if self.should_downscale(plugin): + self._log.debug( + "image needs rescaling ({} > {})", self.size[0], plugin.maxwidth ) - elif check == self.CANDIDATE_DOWNSIZE: - # dimensions are correct, so maxwidth is set to maximum dimension - self.path = ArtResizer.shared.resize( - max(self.size), - self.path, - quality=plugin.quality, - max_filesize=plugin.max_filesize, + convert_params["maxwidth"] = plugin.maxwidth + + filesize = os.stat(syspath(self.path)).st_size + if self.should_downsize(plugin, filesize): + self._log.debug( + "image needs resizing ({}B > {}B)", + filesize, + plugin.max_filesize, ) - elif check == self.CANDIDATE_DEINTERLACE: - self.path = ArtResizer.shared.deinterlace(self.path) - elif check == self.CANDIDATE_REFORMAT: - self.path = ArtResizer.shared.reformat( - self.path, + convert_params["max_filesize"] = plugin.max_filesize + + fmt = ArtResizer.shared.get_format(self.path) + if self.should_reformat(plugin, fmt): + self._log.debug( + "image needs reformatting: {} -> {}", + fmt, plugin.cover_format, - deinterlaced=plugin.deinterlace, ) + convert_params["new_format"] = plugin.cover_format + + if self.should_deinterlace(plugin): + self._log.debug("image needs deinterlacing") + convert_params["deinterlace"] = plugin.deinterlace + + if convert_params: + self.path = ArtResizer.shared.convert(self.path, **convert_params) def _logged_get(log, *args, **kwargs): @@ -1410,7 +1366,7 @@ def art_for_album(self, album, paths, local_only=False): break if out: - out.resize(self) + out.convert(self) return out diff --git a/docs/changelog.rst b/docs/changelog.rst index 33a4b5f94d..7780d116c6 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -40,6 +40,7 @@ Bug fixes: issues in the future. :bug:`5289` * :doc:`plugins/discogs`: Fix the ``TypeError`` when there is no description. +* Fixed fetchart plugin issue where some image manipulation options conflicted with each other. :bug:`4452` For packagers: @@ -80,6 +81,9 @@ Other changes: calculate the bpm. Previously this import was being done immediately, so every ``beet`` invocation was being delayed by a couple of seconds. :bug:`5185` +* :doc:`plugins/fetchart`: Consolidated fetchart image manipulation options to + be handled all in one place. See + https://github.com/beetbox/beets/pull/4133#issuecomment-968268133 2.0.0 (May 30, 2024) -------------------- diff --git a/test/plugins/test_art.py b/test/plugins/test_art.py index acb7123548..22a86f873d 100644 --- a/test/plugins/test_art.py +++ b/test/plugins/test_art.py @@ -844,9 +844,9 @@ class ArtForAlbumTest(UseThePlugin): IMG_225x225_SIZE = os.stat(util.syspath(IMG_225x225)).st_size IMG_348x348_SIZE = os.stat(util.syspath(IMG_348x348)).st_size - RESIZE_OP = "resize" - DEINTERLACE_OP = "deinterlace" - REFORMAT_OP = "reformat" + RESIZE_OP = "convert" + DEINTERLACE_OP = "convert" + REFORMAT_OP = "convert" def setUp(self): super().setUp() diff --git a/test/rsrc/greyskies.png b/test/rsrc/greyskies.png new file mode 100644 index 0000000000..0a4c52f161 Binary files /dev/null and b/test/rsrc/greyskies.png differ diff --git a/test/test_art_resize.py b/test/test_art_resize.py index 8dd4d0e895..9c699e22b6 100644 --- a/test/test_art_resize.py +++ b/test/test_art_resize.py @@ -53,24 +53,25 @@ class ArtResizerFileSizeTest(CleanupModulesMixin, BeetsTestCase): modules = (IMBackend.__module__,) IMG_225x225 = os.path.join(_common.RSRC, b"abbey.jpg") + IMG_225x225_PNG = os.path.join(_common.RSRC, b"greyskies.png") IMG_225x225_SIZE = os.stat(syspath(IMG_225x225)).st_size def _test_img_resize(self, backend): """Test resizing based on file size, given a resize_func.""" # Check quality setting unaffected by new parameter - im_95_qual = backend.resize( - 225, + im_95_qual = backend.convert( self.IMG_225x225, + maxwidth=225, quality=95, max_filesize=0, ) - # check valid path returned - max_filesize hasn't broken resize command + # check valid path returned - max_filesize hasn't broken convert command self.assertExists(im_95_qual) # Attempt a lower filesize with same quality - im_a = backend.resize( - 225, + im_a = backend.convert( self.IMG_225x225, + maxwidth=225, quality=95, max_filesize=0.9 * os.stat(syspath(im_95_qual)).st_size, ) @@ -82,17 +83,17 @@ def _test_img_resize(self, backend): ) # Attempt with lower initial quality - im_75_qual = backend.resize( - 225, + im_75_qual = backend.convert( self.IMG_225x225, + maxwidth=225, quality=75, max_filesize=0, ) self.assertExists(im_75_qual) - im_b = backend.resize( - 225, + im_b = backend.convert( self.IMG_225x225, + maxwidth=225, quality=95, max_filesize=0.9 * os.stat(syspath(im_75_qual)).st_size, ) @@ -103,15 +104,71 @@ def _test_img_resize(self, backend): < os.stat(syspath(im_75_qual)).st_size ) + def _test_img_reformat(self, backend): + fname, ext = os.path.splitext(self.IMG_225x225) + target_png = fname + b"." + "png".encode("utf8") + # check reformat converts jpg to png + im_png = backend.convert( + self.IMG_225x225, + target=target_png, + ) + assert backend.get_format(im_png) == b"PNG" + + # check reformat converts png to jpg with deinterlaced and maxwidth option + fname, ext = os.path.splitext(self.IMG_225x225_PNG) + target_jpg = fname + b"." + "jpg".encode("utf8") + im_jpg_deinterlaced = backend.convert( + self.IMG_225x225_PNG, + maxwidth=225, + target=target_jpg, + deinterlaced=True, + ) + + assert backend.get_format(im_jpg_deinterlaced) == b"JPEG" + self._test_img_deinterlaced(backend, im_jpg_deinterlaced) + + # check reformat actually also resizes if maxwidth is also passed in + im_png_deinterlaced_smaller = backend.convert( + self.IMG_225x225_PNG, + maxwidth=100, + deinterlaced=True, + ) + + assert backend.get_format(im_png_deinterlaced_smaller) == b"PNG" + assert ( + os.stat(syspath(im_png_deinterlaced_smaller)).st_size + < os.stat(syspath(self.IMG_225x225_PNG)).st_size + ) + self._test_img_deinterlaced(backend, im_png_deinterlaced_smaller) + + def _test_img_deinterlaced(self, backend, path): + if backend.NAME == "PIL": + from PIL import Image + + with Image.open(path) as img: + assert "progression" not in img.info + elif backend.NAME == "IMImageMagick": + cmd = backend.identify_cmd + [ + "-format", + "%[interlace]", + syspath(path, prefix=False), + ] + out = command_output(cmd).stdout + assert out == b"None" + @unittest.skipUnless(PILBackend.available(), "PIL not available") - def test_pil_file_resize(self): + def test_pil_file_convert(self): """Test PIL resize function is lowering file size.""" self._test_img_resize(PILBackend()) + """Test PIL convert function is changing the file format""" + self._test_img_reformat(PILBackend()) @unittest.skipUnless(IMBackend.available(), "ImageMagick not available") - def test_im_file_resize(self): - """Test IM resize function is lowering file size.""" + def test_im_file_convert(self): + """Test IM convert function is lowering file size.""" self._test_img_resize(IMBackend()) + """Test IM convert function is changing the file format""" + self._test_img_reformat(IMBackend()) @unittest.skipUnless(PILBackend.available(), "PIL not available") def test_pil_file_deinterlace(self): @@ -120,11 +177,9 @@ def test_pil_file_deinterlace(self): Check if the `PILBackend.deinterlace()` function returns images that are non-progressive """ - path = PILBackend().deinterlace(self.IMG_225x225) - from PIL import Image - - with Image.open(path) as img: - assert "progression" not in img.info + pil = PILBackend() + path = pil.convert(self.IMG_225x225, deinterlaced=True) + self._test_img_deinterlaced(pil, path) @unittest.skipUnless(IMBackend.available(), "ImageMagick not available") def test_im_file_deinterlace(self): @@ -134,14 +189,8 @@ def test_im_file_deinterlace(self): that are non-progressive. """ im = IMBackend() - path = im.deinterlace(self.IMG_225x225) - cmd = im.identify_cmd + [ - "-format", - "%[interlace]", - syspath(path, prefix=False), - ] - out = command_output(cmd).stdout - assert out == b"None" + path = im.convert(self.IMG_225x225, deinterlaced=True) + self._test_img_deinterlaced(im, path) @patch("beets.util.artresizer.util") def test_write_metadata_im(self, mock_util):