Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CoverArt Post Processing #347

Open
wants to merge 4 commits into
base: 2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions plugins/coverart_processing/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
PLUGIN_NAME = "CoverArt Processing"
PLUGIN_AUTHOR = "Pranay"
PLUGIN_DESCRIPTION = """
Post Processing Features for the CoverArt Images.
Ignore images smaller than specified width and height.
Resize the image if larger than specified dimensions.
"""
PLUGIN_VERSION = '0.1'
PLUGIN_API_VERSIONS = ['2.2']
PLUGIN_LICENSE = "GPL-2.0"
PLUGIN_LICENSE_URL = "https://www.gnu.org/licenses/gpl-2.0.html"

from PIL import Image
from io import BytesIO
from picard.plugins.coverart_processing.ui_options_coverart_processing import Ui_CoverartProcessingOptionsPage
from picard.util import imageinfo
from picard import log, config
from picard.metadata import register_track_metadata_processor
from picard.ui.options import register_options_page, OptionsPage
from picard.plugin import PluginPriority

MAX_DIMENSION_DEFAULT = 600 # Set default maximum allowable dimensions of image in pixels
MIN_DIMENSION_DEFAULT = 400 # Set default minimum allowable dimensions of image in pixels
MAX_DIMENSION_RANGE = 2000

class CoverArtProcessor:
def __init__(self):
self.max_dimension = int(config.setting['max_dimension']) if ('max_dimension' in config.setting and config.setting['max_dimension'] is not None) else MAX_DIMENSION_DEFAULT
self.min_dimension = int(config.setting['min_dimension']) if ('min_dimension' in config.setting and config.setting['min_dimension'] is not None) else MIN_DIMENSION_DEFAULT
self.cache = {}

def ignore_image(self, img_data):
"""Ignore The image file if the dimensions are smaller than predefined"""
(width, height) = imageinfo.identify(img_data)[:2]
return width < self.min_dimension or height < self.max_dimension

def resize_image(self, image_data):
"""Resize the image to max_size if larger than max_size."""
with Image.open(BytesIO(image_data)) as img:
width, height = img.size

if width > self.max_dimension or height > self.max_dimension:
# Calculate the new size while maintaining aspect ratio
aspect_ratio = width / height
if width > height:
new_width = self.max_dimension
new_height = int(new_width / aspect_ratio)
else:
new_height = self.max_dimension
new_width = int(new_height * aspect_ratio)

img = img.resize((new_width, new_height), Image.LANCZOS)

with BytesIO() as output:
img.save(output, format="JPEG", quality=85)
return output.getvalue()

# If image dimensions are already within the max limit, original image is to be used.
return image_data

def process_images(self, album, metadata, track, release):
"""Process the Cover Art Images one by one"""
try:
for image_index, image in enumerate(metadata.images):

image_hash = image.url.toString()
if image_hash in self.cache:
if self.cache[image_hash] is None:
metadata.images.pop(image_index)
else:
metadata.images[image_index].set_data(self.cache[image_hash])

else:
if self.ignore_image(image.data):
metadata.images.pop(image_index)
self.cache[image_hash] = None
log.warning(f"{PLUGIN_NAME}: Ignoring Image {image_index+1} because it is smaller than the minimum allowed dimensions.")
else:
new_image_data = self.resize_image(image.data)
metadata.images[image_index].set_data(new_image_data)
self.cache[image_hash] = new_image_data
log.info(f"{PLUGIN_NAME}: Resizing Image {image_index+1}.")
except Exception as ex:
log.error("{0}: Error: {1}".format(PLUGIN_NAME, ex,))


# register the plugin track metadata processor with a higher priority so that other image processing
# plugins can use the resized images.
coverart_processing = CoverArtProcessor()
register_track_metadata_processor(coverart_processing.process_images, priority=PluginPriority.HIGH)

class coverartprocessingOptionsPage(OptionsPage):
NAME = "coverart_processing"
TITLE = "Cover Art Processing"
PARENT = "plugins"

options = [
config.IntOption("setting", "min_dimension", 400),
config.IntOption("setting", "max_dimension", 600),
]

def __init__(self, parent=None):
super().__init__(parent)
self.ui = Ui_CoverartProcessingOptionsPage()
self.ui.setupUi(self)

def load(self):
settings = config.setting
self.ui.spinBox.setMinimum(0)
self.ui.spinBox.setMaximum(MAX_DIMENSION_RANGE)
self.ui.spinBox.setValue(settings["min_dimension"])
self.ui.spinBox_2.setMinimum(0)
self.ui.spinBox_2.setMaximum(MAX_DIMENSION_RANGE)
self.ui.spinBox_2.setValue(settings["max_dimension"])

def save(self):
self._set_settings(config.setting)
coverart_processing.min_dimension = int(self.ui.spinBox.value())
coverart_processing.max_dimension = int(self.ui.spinBox_2.value())
coverart_processing.cache = {}

def _set_settings(self, settings):
settings["min_dimension"] = str(self.ui.spinBox.value())
settings["max_dimension"] = str(self.ui.spinBox_2.value())


register_options_page(coverartprocessingOptionsPage) # Register the plugin options page
41 changes: 41 additions & 0 deletions plugins/coverart_processing/ui_options_coverart_processing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'plugins/format_performer_tags/ui_options_format_performer_tags.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.

from PyQt5 import QtCore, QtWidgets

class Ui_CoverartProcessingOptionsPage(object):
def setupUi(self, Dialog):
Dialog.setObjectName("Dialog")
Dialog.resize(400, 300)
self.buttonBox = QtWidgets.QDialogButtonBox(Dialog)
self.buttonBox.setGeometry(QtCore.QRect(30, 230, 341, 32))
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.spinBox = QtWidgets.QSpinBox(Dialog)
self.spinBox.setGeometry(QtCore.QRect(240, 30, 42, 22))
self.spinBox.setObjectName("spinBox")
self.spinBox_2 = QtWidgets.QSpinBox(Dialog)
self.spinBox_2.setGeometry(QtCore.QRect(240, 100, 42, 22))
self.spinBox_2.setObjectName("spinBox_2")
self.label = QtWidgets.QLabel(Dialog)
self.label.setGeometry(QtCore.QRect(50, 30, 151, 21))
self.label.setObjectName("label")
self.label_2 = QtWidgets.QLabel(Dialog)
self.label_2.setGeometry(QtCore.QRect(50, 100, 151, 21))
self.label_2.setObjectName("label_2")

self.retranslateUi(Dialog)
QtCore.QMetaObject.connectSlotsByName(Dialog)

def retranslateUi(self, Dialog):
_translate = QtCore.QCoreApplication.translate
Dialog.setWindowTitle(_translate("Dialog", "Dialog"))
self.label.setText(_translate("Dialog", "Minimum dimension"))
self.label_2.setText(_translate("Dialog", "Maximum dimension"))