mirror of
https://github.com/beetbox/beets.git
synced 2025-12-06 08:39:17 +01:00
artresizer: move resize functions to backend classes
This commit is contained in:
parent
8a969e3041
commit
c45d2e28a6
2 changed files with 99 additions and 114 deletions
|
|
@ -132,6 +132,47 @@ class IMBackend(LocalBackend):
|
|||
self.identify_cmd = ['magick', 'identify']
|
||||
self.compare_cmd = ['magick', 'compare']
|
||||
|
||||
def resize(self, maxwidth, path_in, path_out=None, quality=0,
|
||||
max_filesize=0):
|
||||
"""Resize using ImageMagick.
|
||||
|
||||
Use the ``magick`` program or ``convert`` on older versions. Return
|
||||
the output path of resized image.
|
||||
"""
|
||||
path_out = path_out or temp_file_for(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',
|
||||
]
|
||||
|
||||
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))
|
||||
return path_in
|
||||
|
||||
return path_out
|
||||
|
||||
|
||||
class PILBackend(LocalBackend):
|
||||
NAME="PIL"
|
||||
|
|
@ -151,112 +192,63 @@ class PILBackend(LocalBackend):
|
|||
"""
|
||||
self.version()
|
||||
|
||||
def resize(self, maxwidth, path_in, path_out=None, quality=0,
|
||||
max_filesize=0):
|
||||
"""Resize using Python Imaging Library (PIL). Return the output path
|
||||
of resized image.
|
||||
"""
|
||||
path_out = path_out or temp_file_for(path_in)
|
||||
from PIL import Image
|
||||
|
||||
def pil_resize(backend, maxwidth, path_in, path_out=None, quality=0,
|
||||
max_filesize=0):
|
||||
"""Resize using Python Imaging Library (PIL). Return the output path
|
||||
of resized image.
|
||||
"""
|
||||
path_out = path_out or temp_file_for(path_in)
|
||||
from PIL import Image
|
||||
log.debug('artresizer: PIL resizing {0} to {1}',
|
||||
displayable_path(path_in), displayable_path(path_out))
|
||||
|
||||
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.ANTIALIAS)
|
||||
|
||||
try:
|
||||
im = Image.open(syspath(path_in))
|
||||
size = maxwidth, maxwidth
|
||||
im.thumbnail(size, Image.ANTIALIAS)
|
||||
if quality == 0:
|
||||
# Use PIL's default quality.
|
||||
quality = -1
|
||||
|
||||
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(py3_path(path_out), quality=quality, progressive=False)
|
||||
|
||||
# progressive=False only affects JPEGs and is the default,
|
||||
# but we include it here for explicitness.
|
||||
im.save(py3_path(path_out), quality=quality, progressive=False)
|
||||
if max_filesize > 0:
|
||||
# If maximum filesize is set, we attempt to lower the quality of
|
||||
# jpeg conversion by a proportional amount, up to 3 attempts
|
||||
# First, set the maximum quality to either provided, or 95
|
||||
if quality > 0:
|
||||
lower_qual = quality
|
||||
else:
|
||||
lower_qual = 95
|
||||
for i in range(5):
|
||||
# 5 attempts is an abitrary choice
|
||||
filesize = os.stat(syspath(path_out)).st_size
|
||||
log.debug("PIL Pass {0} : Output size: {1}B", i, filesize)
|
||||
if filesize <= max_filesize:
|
||||
return path_out
|
||||
# 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(py3_path(path_out), quality=lower_qual,
|
||||
optimize=True, progressive=False)
|
||||
log.warning("PIL Failed to resize file to below {0}B",
|
||||
max_filesize)
|
||||
return path_out
|
||||
|
||||
if max_filesize > 0:
|
||||
# If maximum filesize is set, we attempt to lower the quality of
|
||||
# jpeg conversion by a proportional amount, up to 3 attempts
|
||||
# First, set the maximum quality to either provided, or 95
|
||||
if quality > 0:
|
||||
lower_qual = quality
|
||||
else:
|
||||
lower_qual = 95
|
||||
for i in range(5):
|
||||
# 5 attempts is an abitrary choice
|
||||
filesize = os.stat(syspath(path_out)).st_size
|
||||
log.debug("PIL Pass {0} : Output size: {1}B", i, filesize)
|
||||
if filesize <= max_filesize:
|
||||
return path_out
|
||||
# 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(py3_path(path_out), quality=lower_qual,
|
||||
optimize=True, progressive=False)
|
||||
log.warning("PIL Failed to resize file to below {0}B",
|
||||
max_filesize)
|
||||
return path_out
|
||||
|
||||
else:
|
||||
return path_out
|
||||
except OSError:
|
||||
log.error("PIL cannot create thumbnail for '{0}'",
|
||||
displayable_path(path_in))
|
||||
return path_in
|
||||
|
||||
|
||||
def im_resize(backend, maxwidth, path_in, path_out=None, quality=0,
|
||||
max_filesize=0):
|
||||
"""Resize using ImageMagick.
|
||||
|
||||
Use the ``magick`` program or ``convert`` on older versions. Return
|
||||
the output path of resized image.
|
||||
"""
|
||||
path_out = path_out or temp_file_for(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 = backend.convert_cmd + [
|
||||
syspath(path_in, prefix=False),
|
||||
'-resize', f'{maxwidth}x>',
|
||||
'-interlace', 'none',
|
||||
]
|
||||
|
||||
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))
|
||||
return path_in
|
||||
|
||||
return path_out
|
||||
|
||||
|
||||
BACKEND_FUNCS = {
|
||||
PIL: pil_resize,
|
||||
IMAGEMAGICK: im_resize,
|
||||
}
|
||||
return path_out
|
||||
except OSError:
|
||||
log.error("PIL cannot create thumbnail for '{0}'",
|
||||
displayable_path(path_in))
|
||||
return path_in
|
||||
|
||||
|
||||
def pil_getsize(backend, path_in):
|
||||
|
|
@ -515,8 +507,7 @@ class ArtResizer(metaclass=Shareable):
|
|||
For WEBPROXY, returns `path_in` unmodified.
|
||||
"""
|
||||
if self.local:
|
||||
func = BACKEND_FUNCS[self.local_method]
|
||||
return func(self.local_method, maxwidth, path_in, path_out,
|
||||
return self.local_method.resize(maxwidth, path_in, path_out,
|
||||
quality=quality, max_filesize=max_filesize)
|
||||
else:
|
||||
# Handled by `proxy_url` already.
|
||||
|
|
|
|||
|
|
@ -24,8 +24,6 @@ from beets.util import command_output, syspath
|
|||
from beets.util.artresizer import (
|
||||
IMBackend,
|
||||
PILBackend,
|
||||
pil_resize,
|
||||
im_resize,
|
||||
pil_deinterlace,
|
||||
im_deinterlace,
|
||||
)
|
||||
|
|
@ -64,11 +62,10 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
|||
"""Called after each test, unloading all plugins."""
|
||||
self.teardown_beets()
|
||||
|
||||
def _test_img_resize(self, backend, resize_func):
|
||||
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 = resize_func(
|
||||
backend,
|
||||
im_95_qual = backend.resize(
|
||||
225,
|
||||
self.IMG_225x225,
|
||||
quality=95,
|
||||
|
|
@ -78,8 +75,7 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
|||
self.assertExists(im_95_qual)
|
||||
|
||||
# Attempt a lower filesize with same quality
|
||||
im_a = resize_func(
|
||||
backend,
|
||||
im_a = backend.resize(
|
||||
225,
|
||||
self.IMG_225x225,
|
||||
quality=95,
|
||||
|
|
@ -91,8 +87,7 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
|||
os.stat(syspath(im_95_qual)).st_size)
|
||||
|
||||
# Attempt with lower initial quality
|
||||
im_75_qual = resize_func(
|
||||
backend,
|
||||
im_75_qual = backend.resize(
|
||||
225,
|
||||
self.IMG_225x225,
|
||||
quality=75,
|
||||
|
|
@ -100,8 +95,7 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
|||
)
|
||||
self.assertExists(im_75_qual)
|
||||
|
||||
im_b = resize_func(
|
||||
backend,
|
||||
im_b = backend.resize(
|
||||
225,
|
||||
self.IMG_225x225,
|
||||
quality=95,
|
||||
|
|
@ -115,12 +109,12 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
|||
@unittest.skipUnless(PILBackend.available(), "PIL not available")
|
||||
def test_pil_file_resize(self):
|
||||
"""Test PIL resize function is lowering file size."""
|
||||
self._test_img_resize(PILBackend(), pil_resize)
|
||||
self._test_img_resize(PILBackend())
|
||||
|
||||
@unittest.skipUnless(IMBackend.available(), "ImageMagick not available")
|
||||
def test_im_file_resize(self):
|
||||
"""Test IM resize function is lowering file size."""
|
||||
self._test_img_resize(IMBackend(), im_resize)
|
||||
self._test_img_resize(IMBackend())
|
||||
|
||||
@unittest.skipUnless(PILBackend.available(), "PIL not available")
|
||||
def test_pil_file_deinterlace(self):
|
||||
|
|
|
|||
Loading…
Reference in a new issue