diff --git a/sigal/__init__.py b/sigal/__init__.py index 5ecbd69..0ef79fe 100644 --- a/sigal/__init__.py +++ b/sigal/__init__.py @@ -173,9 +173,13 @@ def build(source, destination, debug, verbose, quiet, force, config, theme, return '{} {}s{}'.format(stats[_type], _type, opt) if not quiet: - print('Done, processed {} and {} in {:.2f} seconds.' - .format(format_stats('image'), format_stats('video'), - time.time() - start_time)) + stats_str = '' + types = sorted(set(t.rsplit('_',1)[0] for t in stats)) + for t in types[:-1]: + stats_str += '{} and '.format(format_stats(t)) + stats_str += '{}'.format(format_stats(types[-1])) + print('Done, processed {} in {:.2f} seconds.' + .format(stats_str, time.time() - start_time)) def init_plugins(settings): diff --git a/sigal/gallery.py b/sigal/gallery.py index a2edff1..3b362a5 100644 --- a/sigal/gallery.py +++ b/sigal/gallery.py @@ -42,7 +42,7 @@ from PIL import Image as PILImage from . import image, signals, video from .image import get_exif_tags, get_image_metadata, get_size, process_image -from .settings import get_thumb +from .settings import Status, get_thumb from .utils import (Devnull, cached_property, check_or_create_dir, copy, get_mime, is_valid_html5_video, read_markdown, url_from_path) @@ -340,15 +340,22 @@ class Album: for f in filenames: ext = splitext(f)[1] + media = None if ext.lower() in settings['img_extensions']: media = Image(f, self.path, settings) elif ext.lower() in settings['video_extensions']: media = Video(f, self.path, settings) - else: - continue - self.medias_count[media.type] += 1 - medias.append(media) + # Allow modification of the media, including overriding the class + # type for the media. + result = signals.album_file.send(self, filename=f, media=media) + for recv, ret in result: + if ret is not None: + media = ret + + if media: + self.medias_count[media.type] += 1 + medias.append(media) signals.album_initialized.send(self) @@ -835,8 +842,23 @@ class Gallery: def process_file(media): - processor = process_image if media.type == 'image' else process_video - return processor(media) + processor = None + if media.type == 'image': + processor = process_image + elif media.type == 'video': + processor = process_video + + # Allow overriding of the processor + result = signals.process_file.send(media, processor=processor) + for recv, ret in result: + if ret is not None: + processor = ret + + if processor: + return processor(media) + else: + logging.warning('Processor not found for media %s', media.path) + return Status.FAILURE def worker(args): diff --git a/sigal/plugins/nonmedia_files.py b/sigal/plugins/nonmedia_files.py new file mode 100644 index 0000000..6f177e9 --- /dev/null +++ b/sigal/plugins/nonmedia_files.py @@ -0,0 +1,139 @@ +"""Plugin to index non-media files. + +This plugin will copy the files into the build tree and generate generic +thumbnails for the files. In-browser previews will likely fail, and +it is up to the theme to provide correct support for downloads. + +Settings available as dictionary in ``nonmedia_files_options``: + +- ``ext_as_thumb``: Enable simple thumbnail showing ext. + Default to ``True`` +- ``ignore_ext``: List of file extensions to ignore. + Default to ``[".md"]`` + +""" + +import logging +import os + +from PIL import Image as PILImage +from PIL import ImageDraw, ImageFont + +from pilkit.utils import save_image + +from sigal import signals +from sigal import utils +from sigal.gallery import Media +from sigal.settings import Status + +logger = logging.getLogger(__name__) + + +DEFAULT_CONFIG = { + 'ext_as_thumb': True, + 'ignore_ext': ['.md'], +} + + +COMMON_MIME_TYPES = { + '.azw': 'application/vnd.amazon.ebook', + '.csv': 'text/csv', + '.epub': 'application/epub+zip', + '.pdf': 'application/pdf', + '.svg': 'image/svg+xml', + '.txt': 'text/plain', + '.zip': 'application/zip', +} + + +def get_mime(ext): + if ext in COMMON_MIME_TYPES: + return COMMON_MIME_TYPES[ext] + return 'application/octet-stream' + + +class NonMedia(Media): + """Gather all informations on a non-media file.""" + + type = 'nonmedia' + + def __init__(self, filename, path, settings): + super().__init__(filename, path, settings) + self.thumb_name = os.path.splitext(self.thumb_name)[0] + '.jpg' + self.date = self._get_file_date() + self.mime = get_mime(self.src_ext) + logger.debug('mime type %s', self.mime) + + @property + def thumbnail(self): + """Path to the thumbnail image (relative to the album directory).""" + if not os.path.isfile(self.thumb_path): + generate_thumbnail(self.src_ext[1:].upper(), self.thumb_path, + self.settings['thumb_size']) + return super().thumbnail + + +def generate_thumbnail(text, outname, box, options=None): + """Create a thumbnail image.""" + img = PILImage.new("RGB", box, (255, 255, 255)) + + fnt = ImageFont.truetype("Pillow/Tests/fonts/FreeMono.ttf", 40) + anchor = (box[0] // 2, box[1] // 2) + d = ImageDraw.Draw(img) + d.text(anchor, text, font=fnt, fill=(0, 0, 0), anchor='mm') + + outformat = 'JPEG' + logger.info('Save thumnail image: %s (%s)', outname, outformat) + save_image(img, outname, outformat, options=options, autoconvert=True) + + +def process_nonmedia(media): + """Process a non-media file: copy and create thumbnail.""" + logger.info('Processing non-media file: %s', media.dst_filename) + settings = media.settings + plugin_settings = settings.get('nonmedia_files_settings', {}) + + try: + utils.copy(media.src_path, media.dst_path, + symlink=settings['orig_link']) + except Exception: + if logger.getEffectiveLevel() == logging.DEBUG: + raise + else: + return Status.FAILURE + + if plugin_settings.get('ext_as_thumb', DEFAULT_CONFIG['ext_as_thumb']): + try: + generate_thumbnail( + media.src_ext[1:].upper(), + media.thumb_path, + settings['thumb_size'], + options=settings['jpg_options'], + ) + except Exception: + if logger.getEffectiveLevel() == logging.DEBUG: + raise + else: + return Status.FAILURE + + +def album_file(album, filename, media=None): + if not media: + ext = os.path.splitext(filename)[1] + ext_ignore = album.settings.get('nonmedia_files_settings', {}).get( + 'ignore_ext', DEFAULT_CONFIG['ignore_ext']) + if ext in ext_ignore: + logger.info('Ignoring non-media file: %s', filename) + else: + logger.info('Registering non-media file: %s', filename) + return NonMedia(filename, album.path, album.settings) + + +def process_file(media, processor=None): + if media.type == 'nonmedia': + return process_nonmedia + + +def register(settings): + signals.album_file.connect(album_file) + signals.process_file.connect(process_file) diff --git a/sigal/signals.py b/sigal/signals.py index 9f1f0e4..bf923de 100644 --- a/sigal/signals.py +++ b/sigal/signals.py @@ -9,3 +9,5 @@ media_initialized = signal('media_initialized') albums_sorted = signal('albums_sorted') medias_sorted = signal('medias_sorted') before_render = signal('before_render') +album_file = signal('album_file') +process_file = signal('process_file') diff --git a/sigal/templates/sigal.conf.py b/sigal/templates/sigal.conf.py index ee4ce42..ed8a4f3 100644 --- a/sigal/templates/sigal.conf.py +++ b/sigal/templates/sigal.conf.py @@ -273,6 +273,7 @@ ignore_files = [] # 'sigal.plugins.feeds', # 'sigal.plugins.media_page', # 'sigal.plugins.nomedia', +# 'sigal.plugins.nonmedia_files', # 'sigal.plugins.upload_s3', # 'sigal.plugins.watermark', # 'sigal.plugins.zip_gallery', @@ -299,6 +300,12 @@ ignore_files = [] # 'ask_password': False # } +# Settings for nonmedia_files plugin +# nonmedia_files_options = { +# 'ext_as_thumb': True, +# 'ignore_ext': ['.md'], +# } + # Settings for upload to s3 plugin # upload_s3_options = { # 'bucket': 'my-bucket', diff --git a/tests/sample/pictures/nonmedia_files/dummy.pdf b/tests/sample/pictures/nonmedia_files/dummy.pdf new file mode 100644 index 0000000..774c2ea Binary files /dev/null and b/tests/sample/pictures/nonmedia_files/dummy.pdf differ diff --git a/tests/test_plugins.py b/tests/test_plugins.py index c81776a..90b0f05 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -27,3 +27,22 @@ def test_plugins(settings, tmpdir, disconnect_signals): for file in files: assert "ignore" not in file + + +def test_nonmedia_files(settings, tmpdir, disconnect_signals): + + settings['destination'] = str(tmpdir) + settings['plugins'] += ['sigal.plugins.nonmedia_files'] + + init_plugins(settings) + + gal = Gallery(settings) + gal.build() + + outfile = os.path.join(settings['destination'], + 'nonmedia_files', 'dummy.pdf') + assert os.path.isfile(outfile) + + outthumb = os.path.join(settings['destination'], + 'nonmedia_files', 'thumbnails', 'dummy.tn.jpg') + assert os.path.isfile(outthumb)