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.identify_cmd = ['magick', 'identify']
|
||||||
self.compare_cmd = ['magick', 'compare']
|
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):
|
class PILBackend(LocalBackend):
|
||||||
NAME="PIL"
|
NAME="PIL"
|
||||||
|
|
@ -151,112 +192,63 @@ class PILBackend(LocalBackend):
|
||||||
"""
|
"""
|
||||||
self.version()
|
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,
|
log.debug('artresizer: PIL resizing {0} to {1}',
|
||||||
max_filesize=0):
|
displayable_path(path_in), displayable_path(path_out))
|
||||||
"""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}',
|
try:
|
||||||
displayable_path(path_in), displayable_path(path_out))
|
im = Image.open(syspath(path_in))
|
||||||
|
size = maxwidth, maxwidth
|
||||||
|
im.thumbnail(size, Image.ANTIALIAS)
|
||||||
|
|
||||||
try:
|
if quality == 0:
|
||||||
im = Image.open(syspath(path_in))
|
# Use PIL's default quality.
|
||||||
size = maxwidth, maxwidth
|
quality = -1
|
||||||
im.thumbnail(size, Image.ANTIALIAS)
|
|
||||||
|
|
||||||
if quality == 0:
|
# progressive=False only affects JPEGs and is the default,
|
||||||
# Use PIL's default quality.
|
# but we include it here for explicitness.
|
||||||
quality = -1
|
im.save(py3_path(path_out), quality=quality, progressive=False)
|
||||||
|
|
||||||
# progressive=False only affects JPEGs and is the default,
|
if max_filesize > 0:
|
||||||
# but we include it here for explicitness.
|
# If maximum filesize is set, we attempt to lower the quality of
|
||||||
im.save(py3_path(path_out), quality=quality, progressive=False)
|
# 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:
|
else:
|
||||||
lower_qual = 95
|
return path_out
|
||||||
for i in range(5):
|
except OSError:
|
||||||
# 5 attempts is an abitrary choice
|
log.error("PIL cannot create thumbnail for '{0}'",
|
||||||
filesize = os.stat(syspath(path_out)).st_size
|
displayable_path(path_in))
|
||||||
log.debug("PIL Pass {0} : Output size: {1}B", i, filesize)
|
return path_in
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def pil_getsize(backend, path_in):
|
def pil_getsize(backend, path_in):
|
||||||
|
|
@ -515,8 +507,7 @@ class ArtResizer(metaclass=Shareable):
|
||||||
For WEBPROXY, returns `path_in` unmodified.
|
For WEBPROXY, returns `path_in` unmodified.
|
||||||
"""
|
"""
|
||||||
if self.local:
|
if self.local:
|
||||||
func = BACKEND_FUNCS[self.local_method]
|
return self.local_method.resize(maxwidth, path_in, path_out,
|
||||||
return func(self.local_method, maxwidth, path_in, path_out,
|
|
||||||
quality=quality, max_filesize=max_filesize)
|
quality=quality, max_filesize=max_filesize)
|
||||||
else:
|
else:
|
||||||
# Handled by `proxy_url` already.
|
# Handled by `proxy_url` already.
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,6 @@ from beets.util import command_output, syspath
|
||||||
from beets.util.artresizer import (
|
from beets.util.artresizer import (
|
||||||
IMBackend,
|
IMBackend,
|
||||||
PILBackend,
|
PILBackend,
|
||||||
pil_resize,
|
|
||||||
im_resize,
|
|
||||||
pil_deinterlace,
|
pil_deinterlace,
|
||||||
im_deinterlace,
|
im_deinterlace,
|
||||||
)
|
)
|
||||||
|
|
@ -64,11 +62,10 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
||||||
"""Called after each test, unloading all plugins."""
|
"""Called after each test, unloading all plugins."""
|
||||||
self.teardown_beets()
|
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."""
|
"""Test resizing based on file size, given a resize_func."""
|
||||||
# Check quality setting unaffected by new parameter
|
# Check quality setting unaffected by new parameter
|
||||||
im_95_qual = resize_func(
|
im_95_qual = backend.resize(
|
||||||
backend,
|
|
||||||
225,
|
225,
|
||||||
self.IMG_225x225,
|
self.IMG_225x225,
|
||||||
quality=95,
|
quality=95,
|
||||||
|
|
@ -78,8 +75,7 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
||||||
self.assertExists(im_95_qual)
|
self.assertExists(im_95_qual)
|
||||||
|
|
||||||
# Attempt a lower filesize with same quality
|
# Attempt a lower filesize with same quality
|
||||||
im_a = resize_func(
|
im_a = backend.resize(
|
||||||
backend,
|
|
||||||
225,
|
225,
|
||||||
self.IMG_225x225,
|
self.IMG_225x225,
|
||||||
quality=95,
|
quality=95,
|
||||||
|
|
@ -91,8 +87,7 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
||||||
os.stat(syspath(im_95_qual)).st_size)
|
os.stat(syspath(im_95_qual)).st_size)
|
||||||
|
|
||||||
# Attempt with lower initial quality
|
# Attempt with lower initial quality
|
||||||
im_75_qual = resize_func(
|
im_75_qual = backend.resize(
|
||||||
backend,
|
|
||||||
225,
|
225,
|
||||||
self.IMG_225x225,
|
self.IMG_225x225,
|
||||||
quality=75,
|
quality=75,
|
||||||
|
|
@ -100,8 +95,7 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
||||||
)
|
)
|
||||||
self.assertExists(im_75_qual)
|
self.assertExists(im_75_qual)
|
||||||
|
|
||||||
im_b = resize_func(
|
im_b = backend.resize(
|
||||||
backend,
|
|
||||||
225,
|
225,
|
||||||
self.IMG_225x225,
|
self.IMG_225x225,
|
||||||
quality=95,
|
quality=95,
|
||||||
|
|
@ -115,12 +109,12 @@ class ArtResizerFileSizeTest(_common.TestCase, TestHelper):
|
||||||
@unittest.skipUnless(PILBackend.available(), "PIL not available")
|
@unittest.skipUnless(PILBackend.available(), "PIL not available")
|
||||||
def test_pil_file_resize(self):
|
def test_pil_file_resize(self):
|
||||||
"""Test PIL resize function is lowering file size."""
|
"""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")
|
@unittest.skipUnless(IMBackend.available(), "ImageMagick not available")
|
||||||
def test_im_file_resize(self):
|
def test_im_file_resize(self):
|
||||||
"""Test IM resize function is lowering file size."""
|
"""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")
|
@unittest.skipUnless(PILBackend.available(), "PIL not available")
|
||||||
def test_pil_file_deinterlace(self):
|
def test_pil_file_deinterlace(self):
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue