-
Notifications
You must be signed in to change notification settings - Fork 1.8k
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
Mosaic plugin #2352
Mosaic plugin #2352
Changes from 23 commits
419d9a0
a2eb6fa
560d003
8e58a61
346ecbc
f7a5844
0492741
a99b7e9
8a99eea
512031a
e756f98
621427f
19e0958
906bd97
7eaaa99
ffcaf33
5361825
7018307
3e9076b
de57602
60318f1
18c5128
21c59bf
31c7330
29b57fb
18127ce
02aa619
78f19db
9d42728
fae8fcc
ab7cc8f
713c00a
85adbd1
4dbc413
80f77ae
7a3c786
7a08e4a
b38f34b
5841752
bd48559
63cd799
d24b373
95a868b
2c71092
68089ac
61b8329
5f2c47e
ba324df
b8e1c56
05f0072
6a71504
c3771f7
04e2975
c10eb8f
02bd19f
24890c7
1ab913b
5a3b74f
6e6dd76
50a2e37
c51ecd4
e3c3798
075e243
2a418a6
d4ff82e
4a17da8
23f172d
ac06be1
bb5629e
287e077
f622e42
813b078
e9c3d69
dd7b129
8e78cfd
bab99f5
a881922
2bf58a6
1c0b795
84febb1
a165d6c
f8862ac
4e0527f
a85dcd8
d88cabc
fa9262d
3e3ad69
11eb90c
3e38a33
07af27e
7dab9f3
b3fbdba
f53ab80
49e548b
471d46d
8b4b64d
8f32bfe
fc6b65d
ce48578
e7ea7ab
9394e0a
22f07b9
409f070
6fa1651
908b8dc
b57c49d
376c31a
faf8be0
6ab7ad2
fc560ac
90c30d8
167ae91
09ea544
f5b23ff
0dc948d
51835e7
c4ef23d
e444143
8f62e8b
169cf59
7c91989
0cb643f
9ccaad2
5eca518
d0522d8
3c852d3
9840964
060041a
35dd6fd
9fe171c
ddfe442
2685f13
8168a88
0148070
a93414d
348e044
1da972f
569098a
91722ae
b5a8891
fbb868e
c648781
e1101d4
95eeec9
ee46a51
a43f5fd
0e7a0a6
f6830b4
b1d8321
291b287
a559e6e
9727ca7
8842bd3
29d6c27
b25eb87
2315287
714560a
730c84e
ca4f96e
0e47095
7cacae5
61fa3ee
f6dc981
15a7dfc
1af3729
290162a
f4d3368
389aed8
d645c03
0a73148
dfde9ce
f65653a
28ba733
06f86c8
52d5d23
53d0421
2465898
8555481
2d7962c
762f9ca
2eb4e3d
e0ce507
719b699
3f68445
71d6dc3
4563e3b
1f2b8ce
97cedce
ace5656
d9c8f97
6664b65
92c118f
527d0a8
ae3f9bf
bc8a8ec
cafbb24
f36c70c
338d471
c840fea
009c6a4
2144882
6f97787
949d7fb
26940b6
8a6c8cd
130c581
8ba0060
f2b6801
63692ff
01cf0d0
93f064f
eaae3fb
690ed73
b1b4272
a52d3d5
d31a483
732f017
d210645
2cd3e13
4917bd3
7e8056b
b575a1e
2a9be17
41bd6f9
8085d13
336fdba
5ca43c3
6185c6a
70a2ad3
774fa62
e742f86
a983ea2
887cf4e
0984b83
a34814b
a613f92
55ccf13
7236ba9
b237799
fef8044
fd4892b
a779bb2
252aa40
644a0a3
7a90a88
ae1d7e7
0345985
0f8888e
35dfcc1
7fef4e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,249 @@ | ||
# -*- coding: utf-8 -*- | ||
# This file is part of beets. | ||
# Copyright 2015-2016, Ohm Patel. | ||
# | ||
# Permission is hereby granted, free of charge, to any person obtaining | ||
# a copy of this software and associated documentation files (the | ||
# "Software"), to deal in the Software without restriction, including | ||
# without limitation the rights to use, copy, modify, merge, publish, | ||
# distribute, sublicense, and/or sell copies of the Software, and to | ||
# permit persons to whom the Software is furnished to do so, subject to | ||
# the following conditions: | ||
# | ||
# The above copyright notice and this permission notice shall be | ||
# included in all copies or substantial portions of the Software. | ||
"""Allows beets to create a mosaic from covers.""" | ||
from __future__ import division, absolute_import, print_function | ||
|
||
import os.path | ||
|
||
from beets.plugins import BeetsPlugin | ||
from beets import ui | ||
|
||
from PIL import Image | ||
import math | ||
from parse import * | ||
|
||
# MOSAICFONT = os.path.join(os.path.dirname(__file__), 'FreeSans.ttf') | ||
|
||
|
||
class MosaicCoverArtPlugin(BeetsPlugin): | ||
col_size = 4 | ||
|
||
def __init__(self): | ||
super(MosaicCoverArtPlugin, self).__init__() | ||
|
||
self.config.add({'geometry': '100x100+3+3', | ||
'tile': '', # or e.g. x3 3x | ||
'label': '', | ||
'background': 'ffffff', | ||
'mosaic': 'mosaic.png', | ||
'show_mosaic': False, | ||
'watermark': '', | ||
'watermark_alpha': 0.4}) | ||
|
||
def commands(self): | ||
cmd = ui.Subcommand('mosaic', help=u"create mosaic from coverart") | ||
|
||
cmd.parser.add_option( | ||
u'-m', u'--mosaic', dest='mosaic', metavar='FILE', | ||
action='store', | ||
help=u'save final mosaic picture as FILE' | ||
) | ||
|
||
cmd.parser.add_option( | ||
u'-w', u'--watermark', dest='watermark', | ||
action='store', metavar='FILE', | ||
help=u'add FILE for a picture to blend over mosaic' | ||
) | ||
|
||
cmd.parser.add_option( | ||
u'-a', u'--alpha', dest='watermark_alpha', | ||
action='store', metavar='ALPHA', | ||
help=u'ALPHA value for blending' | ||
) | ||
cmd.parser.add_option( | ||
u'-c', u'--color', dest='background', | ||
action='store', metavar='HEXCOLOR', | ||
help=u'background color as HEXCOLOR' | ||
) | ||
|
||
cmd.parser.add_option( | ||
u'-g', u'--geometry', dest='geometry', | ||
action='store', metavar='GEOMETRY', | ||
help=u'define geometry as <width>x<height>+<marginx>+<marginy>' | ||
) | ||
|
||
def func(lib, opts, args): | ||
self.config.set_args(opts) | ||
|
||
if self.config['mosaic']: | ||
mosaic = opts.mosaic or self.config['mosaic'].get(str) | ||
else: | ||
mosaic = opts.mosaic | ||
|
||
self._log.debug(u'Mosaic: {}', mosaic) | ||
|
||
if self.config['watermark']: | ||
watermark = opts.watermark or self.config['watermark'].get(str) | ||
else: | ||
watermark = opts.watermark | ||
|
||
watermark_alpha = float(opts.watermark_alpha) or self.config[ | ||
'watermark_alpha'].get(float) | ||
|
||
if self.config['background']: | ||
background = opts.background or self.config[ | ||
'background'].get(str) | ||
else: | ||
background = opts.background | ||
|
||
if self.config['geometry']: | ||
geometry = opts.geometry or self.config[ | ||
'geometry'].get(str) | ||
else: | ||
geometry = opts.background | ||
albums = lib.albums(ui.decargs(args)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since you've already used geometry = self.config['geometry'].get(str) In fact, this is probably what you want: geometry = self.config['geometry'].as_str() No need to test whether the value is present; just use it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Of course, you don't even need to pass around stuff like |
||
|
||
self._generate_montage(lib, | ||
albums, | ||
mosaic, | ||
watermark, | ||
background, | ||
watermark_alpha, | ||
geometry) | ||
|
||
cmd.func = func | ||
return [cmd] | ||
|
||
def _generate_montage(self, lib, albums, | ||
fn_mosaic, fn_watermark, | ||
background, watermark_alpha, geometry): | ||
|
||
parsestr = "{cellwidth:d}x{cellheight:d}" | ||
parsestr += "+{cellmarginx:d}+{cellmarginy:d}" | ||
|
||
geo = parse(parsestr, geometry) | ||
|
||
covers = [] | ||
|
||
for album in albums: | ||
|
||
if album.artpath and os.path.exists(album.artpath): | ||
self._log.debug(u'{}', album.artpath) | ||
covers.append(album.artpath) | ||
else: | ||
covers.append("||" + album.albumartist + "\n" + album.album) | ||
self._log.debug(u'#{} has no album?#', album) | ||
|
||
sqrtnum = int(math.sqrt(len(covers))) | ||
|
||
tail = len(covers) - (sqrtnum * sqrtnum) | ||
|
||
rows = cols = sqrtnum | ||
|
||
if tail >= cols: | ||
cols += 1 | ||
|
||
tail = len(covers) - (cols * sqrtnum) | ||
|
||
if tail > 0: | ||
rows += 1 | ||
|
||
self._log.debug(u'Cells: {}x{}', cols, rows) | ||
|
||
mosaic_size_width = geo['cellmarginx'] + (cols * (geo['cellwidth'] + | ||
geo['cellmarginx'])) | ||
|
||
mosaic_size_height = geo['cellmarginy'] + (rows * | ||
(geo['cellheight'] + | ||
geo['cellmarginy'])) | ||
|
||
self._log.debug(u'Mosaic size: {}x{}', | ||
mosaic_size_width, mosaic_size_height) | ||
|
||
montage = Image.new('RGB', (mosaic_size_width, mosaic_size_height), | ||
tuple(int(background[i:i + 2], 16) | ||
for i in (0, 2, 4))) | ||
|
||
size = int(geo['cellwidth']), int(geo['cellheight']) | ||
offset_x = int(geo['cellmarginx']) | ||
offset_y = int(geo['cellmarginy']) | ||
colcounter = 0 | ||
|
||
# fnt = ImageFont.truetype(MOSAICFONT, 12) | ||
|
||
for cover in covers: | ||
|
||
try: | ||
if '||' in cover: | ||
|
||
im = Image.new('RGB', size, | ||
tuple(int(background[i:i + 2], 16) | ||
for i in (0, 2, 4))) | ||
# d = ImageDraw.Draw(im) | ||
# d.multiline_text((10, 10), cover[2:], | ||
# fill=(0, 0, 0), font=fnt, anchor=None, | ||
# spacing=0, align="left") | ||
|
||
else: | ||
im = Image.open(cover) | ||
im.thumbnail(size, Image.ANTIALIAS) | ||
|
||
self._log.debug(u'Paste into mosaic: {} - {}x{}', | ||
cover, offset_x, offset_y) | ||
montage.paste(im, (offset_x, offset_y)) | ||
|
||
colcounter += 1 | ||
if colcounter >= cols: | ||
offset_y += geo['cellwidth'] + geo['cellmarginy'] | ||
offset_x = geo['cellmarginx'] | ||
colcounter = 0 | ||
else: | ||
offset_x += geo['cellwidth'] + geo['cellmarginx'] | ||
|
||
im.close() | ||
except IOError: | ||
self._log.error(u'Problem with {}', cover) | ||
|
||
if fn_watermark: | ||
foreground = Image.open(fn_watermark) | ||
m_width, m_height = montage.size | ||
f_width, f_height = foreground.size | ||
|
||
if f_width > f_height: | ||
d = f_width / 2 - (f_height / 2) | ||
e = f_width / 2 + (f_height / 2) | ||
box = (d, 0, e, f_height) | ||
nf = foreground.crop(box) | ||
elif f_width < f_height: | ||
d = f_height / 2 - (f_width / 2) | ||
e = f_height / 2 + (f_width / 2) | ||
box = (0, d, f_width, e) | ||
nf = foreground.crop(box) | ||
else: | ||
nf = foreground | ||
|
||
longer_side = max(montage.size) | ||
horizontal_padding = (longer_side - montage.size[0]) / 2 | ||
vertical_padding = (longer_side - montage.size[1]) / 2 | ||
img5 = montage.crop( | ||
( | ||
-horizontal_padding, | ||
-vertical_padding, | ||
montage.size[0] + horizontal_padding, | ||
montage.size[1] + vertical_padding | ||
) | ||
) | ||
|
||
m_width, m_height = img5.size | ||
|
||
nf2 = nf.resize(img5.size, Image.ANTIALIAS) | ||
f_width, f_height = nf2.size | ||
|
||
self._log.debug(u'Save montage to: {}:{}-{} {}:{}-{}', | ||
img5.mode, m_width, m_height, nf.mode, | ||
f_width, f_height) | ||
Image.blend(img5, nf2, watermark_alpha).save(fn_mosaic) | ||
else: | ||
montage.save(fn_mosaic) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
Mosaic Plugin | ||
===================== | ||
|
||
The ``mosaic`` plugin generates a montage of a mosiac from cover art. | ||
|
||
To use the ``mosaic`` plugin, first enable it in your configuration (see | ||
:ref:`using-plugins`). Then, install the `Pillow`_ library by typing:: | ||
|
||
pip install Pillow | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks like this plugin also uses a library called |
||
|
||
The plugin uses `Pillow`_ to manipulate album art and create the mosiac. | ||
|
||
.. _pillow: http://pillow.readthedocs.io/en/latest/ | ||
|
||
|
||
By default the ``mosaic`` generates a mosaic, as mosaic.png in the current directory, of cover art out of the whole library . | ||
|
||
You can customize the output mosaic, overlay and blend a image as watermark and use a alternative filename as result picture :: | ||
|
||
-h, --help show this help message and exit | ||
-m FILE, --mosaic=file save final mosaic picture as FILE | ||
-w FILE, --watermark=FILE add FILE for a picture to blend over mosaic | ||
-a ALPHA, --alpha=ALPHA ALPHA value for blending 0.0-1.0 | ||
-c HEXCOLOR, --color=HEXCOLOR background color as HEXCOLOR | ||
-g GEOMETRY, --geometry=GEOMETRY Geometry defined as <width>x<height>+<marginx>+<marginy> | ||
|
||
Examples | ||
-------- | ||
Create mosaic from all Album cover art:: | ||
|
||
$ beet mosaic | ||
|
||
Create mosaic from band Tool with a second picture as watermark:: | ||
|
||
$ beet mosaic -w c:/temp/tool.png -a 0.4 Tool | ||
|
||
Create mosaic from every Album out of year 2012, use background color red:: | ||
|
||
$ beet mosaic -b ff0000 year:2012 | ||
|
||
Configuration | ||
------------- | ||
|
||
To configure the plugin, make a ``mosaic:`` section in your | ||
configuration file. There is four option: | ||
|
||
- **mosaic**: Use a different filename for the final mosaic picture, | ||
Default: ``mosaic.png``. | ||
- **watermark**: Use a picture to blend over the mosiac picture, | ||
Default: ''None''. | ||
- **alpha**: The factor as float, to blend the watermark over the mosaic. 0.0 - means no visible watermark. 1.0 - full visible watermark. | ||
Default: ``0.4`` | ||
- **background**: The color of the background and the visible border of the mosaic as Hexcolor e.g. ffffff for white or 000000 as black | ||
Default: ``ffffff`` | ||
- **geometry**: Define geometry of each cover defined as <width>x<height>+<marginx>+<marginy> | ||
- Default: ``100x100+3+3`` | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's usually best to avoid "star imports," which make it difficult to tell where names come from.