adding the thumbnail and summary plugin

This commit is contained in:
manetta 2020-03-11 15:12:35 +01:00
parent eaeffbcbcb
commit 7535649f56
28 changed files with 1126 additions and 0 deletions

View File

@ -0,0 +1,56 @@
Summary
-------
This plugin allows easy, variable length summaries directly embedded into the
body of your articles. It introduces two new settings: ``SUMMARY_BEGIN_MARKER``
and ``SUMMARY_END_MARKER``: strings which can be placed directly into an article
to mark the beginning and end of a summary. When found, the standard
``SUMMARY_MAX_LENGTH`` setting will be ignored. The markers themselves will also
be removed from your articles before they are published. The default values
are ``<!-- PELICAN_BEGIN_SUMMARY -->`` and ``<!-- PELICAN_END_SUMMARY -->``.
For example::
Title: My super title
Date: 2010-12-03 10:20
Tags: thats, awesome
Category: yeah
Slug: my-super-post
Author: Alexis Metaireau
This is the content of my super blog post.
<!-- PELICAN_END_SUMMARY -->
and this content occurs after the summary.
Here, the summary is taken to be the first line of the post. Because no
beginning marker was found, it starts at the top of the body. It is possible
to leave out the end marker instead, in which case the summary will start at the
beginning marker and continue to the end of the body.
If no beginning or end marker is found, and if ``SUMMARY_USE_FIRST_PARAGRAPH``
is enabled in the settings, the summary will be the first paragraph of the post.
The plugin also sets a ``has_summary`` attribute on every article. It is True
for articles with an explicitly-defined summary, and False otherwise. (It is
also False for an article truncated by ``SUMMARY_MAX_LENGTH``.) Your templates
can use this e.g. to add a link to the full text at the end of the summary.
reST example
~~~~~~~~~~~~
Inserting the markers into a reStructuredText document makes use of the
comment directive, because raw HTML is automatically escaped. The reST equivalent of the above Markdown example looks like this::
My super title
##############
:date: 2010-12-03 10:20
:tags: thats, awesome
:category: yeah
:slug: my-super-post
:author: Alexis Metaireau
This is the content of my super blog post.
.. PELICAN_END_SUMMARY
and this content occurs after the summary.

View File

@ -0,0 +1 @@
from .summary import *

View File

@ -0,0 +1,105 @@
"""
Summary
-------
This plugin allows easy, variable length summaries directly embedded into the
body of your articles.
"""
from __future__ import unicode_literals
from pelican import signals
from pelican.generators import ArticlesGenerator, StaticGenerator, PagesGenerator
import re
def initialized(pelican):
from pelican.settings import DEFAULT_CONFIG
DEFAULT_CONFIG.setdefault('SUMMARY_BEGIN_MARKER',
'<!-- PELICAN_BEGIN_SUMMARY -->')
DEFAULT_CONFIG.setdefault('SUMMARY_END_MARKER',
'<!-- PELICAN_END_SUMMARY -->')
DEFAULT_CONFIG.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False)
if pelican:
pelican.settings.setdefault('SUMMARY_BEGIN_MARKER',
'<!-- PELICAN_BEGIN_SUMMARY -->')
pelican.settings.setdefault('SUMMARY_END_MARKER',
'<!-- PELICAN_END_SUMMARY -->')
pelican.settings.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False)
def extract_summary(instance):
# if summary is already specified, use it
# if there is no content, there's nothing to do
if hasattr(instance, '_summary'):
instance.has_summary = True
return
if not instance._content:
instance.has_summary = False
return
begin_marker = instance.settings['SUMMARY_BEGIN_MARKER']
end_marker = instance.settings['SUMMARY_END_MARKER']
use_first_paragraph = instance.settings['SUMMARY_USE_FIRST_PARAGRAPH']
remove_markers = True
content = instance._content
begin_summary = -1
end_summary = -1
if begin_marker:
begin_summary = content.find(begin_marker)
if end_marker:
end_summary = content.find(end_marker)
if begin_summary == -1 and end_summary == -1 and use_first_paragraph:
begin_marker, end_marker = '<p>', '</p>'
remove_markers = False
begin_summary = content.find(begin_marker)
end_summary = content.find(end_marker)
if begin_summary == -1 and end_summary == -1:
instance.has_summary = False
return
# skip over the begin marker, if present
if begin_summary == -1:
begin_summary = 0
else:
begin_summary = begin_summary + len(begin_marker)
if end_summary == -1:
end_summary = None
summary = content[begin_summary:end_summary]
if remove_markers:
# remove the markers from the content
if begin_summary:
content = content.replace(begin_marker, '', 1)
if end_summary:
content = content.replace(end_marker, '', 1)
summary = re.sub(r"<div.*>", "", summary)
summary = re.sub(r"</div>", "", summary)
instance._content = content
instance._summary = summary
instance.has_summary = True
def run_plugin(generators):
for generator in generators:
if isinstance(generator, ArticlesGenerator):
for article in generator.articles:
extract_summary(article)
elif isinstance(generator, PagesGenerator):
for page in generator.pages:
extract_summary(page)
def register():
signals.initialized.connect(initialized)
try:
signals.all_generators_finalized.connect(run_plugin)
except AttributeError:
# NOTE: This results in #314 so shouldn't really be relied on
# https://github.com/getpelican/pelican-plugins/issues/314
signals.content_object_init.connect(extract_summary)

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
import unittest
from jinja2.utils import generate_lorem_ipsum
# generate one paragraph, enclosed with <p>
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)
from pelican.contents import Page
import pelican.settings
import summary
class TestSummary(unittest.TestCase):
def setUp(self):
super(TestSummary, self).setUp()
pelican.settings.DEFAULT_CONFIG['SUMMARY_MAX_LENGTH'] = None
pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = False
summary.register()
summary.initialized(None)
self.page_kwargs = {
'content': TEST_CONTENT,
'context': {
'localsiteurl': '',
},
'metadata': {
'summary': TEST_SUMMARY,
'title': 'foo bar',
'author': 'Blogger',
},
}
def _copy_page_kwargs(self):
# make a deep copy of page_kwargs
page_kwargs = dict([(key, self.page_kwargs[key]) for key in
self.page_kwargs])
for key in page_kwargs:
if not isinstance(page_kwargs[key], dict):
break
page_kwargs[key] = dict([(subkey, page_kwargs[key][subkey])
for subkey in page_kwargs[key]])
return page_kwargs
def test_end_summary(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
page_kwargs['content'] = (
TEST_SUMMARY + '<!-- PELICAN_END_SUMMARY -->' + TEST_CONTENT)
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_SUMMARY)
self.assertEqual(page.content, TEST_SUMMARY + TEST_CONTENT)
def test_begin_summary(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
page_kwargs['content'] = (
'FOOBAR<!-- PELICAN_BEGIN_SUMMARY -->' + TEST_CONTENT)
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_CONTENT)
self.assertEqual(page.content, 'FOOBAR' + TEST_CONTENT)
def test_begin_end_summary(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
page_kwargs['content'] = (
'FOOBAR<!-- PELICAN_BEGIN_SUMMARY -->' + TEST_SUMMARY +
'<!-- PELICAN_END_SUMMARY -->' + TEST_CONTENT)
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_SUMMARY)
self.assertEqual(page.content, 'FOOBAR' + TEST_SUMMARY + TEST_CONTENT)
def test_use_first_paragraph(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = True
page_kwargs['content'] = '<p>' + TEST_SUMMARY + '</p>' + TEST_CONTENT
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_SUMMARY)
self.assertEqual(page.content, '<p>' + TEST_SUMMARY + '</p>' + TEST_CONTENT)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,33 @@
Thumbnail Creation of images
============================
This plugin creates thumbnails for all of the images found under a specific directory, in various thumbnail sizes.
It requires `PIL` to function properly since `PIL` is used to resize the images, and the thumbnail will only be re-built
if it doesn't already exist (to save processing time).
Installation
-------------
Set up like any other plugin, making sure to set `PLUGIN_PATH` and add `thumbnailer` to the `PLUGINS` list.
[PIL](http://www.pythonware.com/products/pil/) or [Pillow](http://pillow.readthedocs.org/en/latest/installation.html#)
is required. If you choose Pillow, you need to install some additional
[external libraries](http://www.pythonware.com/products/pil/) to add support for image types like `jpg`.
Configuration
-------------
* `IMAGE_PATH` is the path to the image directory. It should reside inside your content directory, and defaults to "pictures".
* `THUMBNAIL_DIR` is the path to the output sub-directory where the thumbnails are generated.
* `THUMBNAIL_SIZES` is a dictionary mapping size name to size specifications.
The generated filename will be `originalname_thumbnailname.ext` unless `THUMBNAIL_KEEP_NAME` is set.
* `THUMBNAIL_KEEP_NAME` is a Boolean that, if set, puts the file with the original name in a thumbnailname folder, named like the key in `THUMBNAIL_SIZES`.
* `THUMBNAIL_KEEP_TREE` is a Boolean that, if set, saves the image directory tree.
* `THUMBNAIL_INCLUDE_REGEX` is an optional string that is used as regular expression to restrict thumbnailing to matching files. By default all files not starting with a dot are respected.
Sizes can be specified using any of the following formats:
* wxh will resize to exactly wxh cropping as necessary to get that size
* wx? will resize so that the width is the specified size, and the height will scale to retain aspect ratio
* ?xh same as wx? but will height being a set size
* s is a shorthand for wxh where w=h

View File

@ -0,0 +1 @@
from .thumbnailer import *

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -0,0 +1,63 @@
from thumbnailer import _resizer
from unittest import TestCase, main
import os
from PIL import Image
class ThumbnailerTests(TestCase):
def path(self, filename):
return os.path.join(self.img_path, filename)
def setUp(self):
self.img_path = os.path.join(os.path.dirname(__file__), "test_data")
self.img = Image.open(self.path("sample_image.jpg"))
def testSquare(self):
r = _resizer('square', '100', self.img_path)
output = r.resize(self.img)
self.assertEqual((100, 100), output.size)
def testExact(self):
r = _resizer('exact', '250x100', self.img_path)
output = r.resize(self.img)
self.assertEqual((250, 100), output.size)
def testWidth(self):
r = _resizer('aspect', '250x?', self.img_path)
output = r.resize(self.img)
self.assertEqual((250, 166), output.size)
def testHeight(self):
r = _resizer('aspect', '?x250', self.img_path)
output = r.resize(self.img)
self.assertEqual((375, 250), output.size)
class ThumbnailerFilenameTest(TestCase):
def path(self, *parts):
return os.path.join(self.img_path, *parts)
def setUp(self):
self.img_path = os.path.join(os.path.dirname(__file__), "test_data")
def testRoot(self):
"""Test a file that is in the root of img_path."""
r = _resizer('square', '100', self.img_path)
new_name = r.get_thumbnail_name(self.path('sample_image.jpg'))
self.assertEqual('sample_image_square.jpg', new_name)
def testRootWithSlash(self):
r = _resizer('square', '100', self.img_path + '/')
new_name = r.get_thumbnail_name(self.path('sample_image.jpg'))
self.assertEqual('sample_image_square.jpg', new_name)
def testSubdir(self):
"""Test a file that is in a sub-directory of img_path."""
r = _resizer('square', '100', self.img_path)
new_name = r.get_thumbnail_name(self.path('subdir', 'sample_image.jpg'))
self.assertEqual('subdir/sample_image_square.jpg', new_name)
if __name__=="__main__":
main()

View File

@ -0,0 +1,208 @@
import os
import os.path as path
import re
from pelican import signals
import logging
logger = logging.getLogger(__name__)
try:
from PIL import Image, ImageOps
enabled = True
except ImportError:
logging.warning("Unable to load PIL, disabling thumbnailer")
enabled = False
DEFAULT_IMAGE_DIR = "pictures"
DEFAULT_THUMBNAIL_DIR = "thumbnails"
DEFAULT_THUMBNAIL_SIZES = {
'thumbnail_square': '150',
'thumbnail_wide': '150x?',
'thumbnail_tall': '?x150',
}
DEFAULT_TEMPLATE = """<a href="{url}" rel="shadowbox" title="{filename}"><img src="{thumbnail}" alt="{filename}"></a>"""
DEFAULT_GALLERY_THUMB = "thumbnail_square"
class _resizer(object):
""" Resizes based on a text specification, see readme """
REGEX = re.compile(r'(\d+|\?)x(\d+|\?)')
def __init__(self, name, spec, root):
self._name = name
self._spec = spec
# The location of input images from _image_path.
self._root = root
def _null_resize(self, w, h, image):
return image
def _exact_resize(self, w, h, image):
retval = ImageOps.fit(image, (w,h), Image.BICUBIC)
return retval
def _aspect_resize(self, w, h, image):
retval = image.copy()
retval.thumbnail((w, h), Image.ANTIALIAS)
return retval
def resize(self, image):
resizer = self._null_resize
# Square resize and crop
if 'x' not in self._spec:
resizer = self._exact_resize
targetw = int(self._spec)
targeth = targetw
else:
matches = self.REGEX.search(self._spec)
tmpw = matches.group(1)
tmph = matches.group(2)
# Full Size
if tmpw == '?' and tmph == '?':
targetw = image.size[0]
targeth = image.size[1]
resizer = self._null_resize
# Set Height Size
if tmpw == '?':
targetw = image.size[0]
targeth = int(tmph)
resizer = self._aspect_resize
# Set Width Size
elif tmph == '?':
targetw = int(tmpw)
targeth = image.size[1]
resizer = self._aspect_resize
# Scale and Crop
else:
targetw = int(tmpw)
targeth = int(tmph)
resizer = self._exact_resize
logging.debug("Using resizer {0}".format(resizer.__name__))
return resizer(targetw, targeth, image)
def get_thumbnail_name(self, in_path):
# Find the partial path + filename beyond the input image directory.
prefix = path.commonprefix([in_path, self._root])
new_filename = in_path[len(prefix) + 1:]
# Generate the new filename.
(basename, ext) = path.splitext(new_filename)
return "{0}_{1}{2}".format(basename, self._name, ext)
def resize_file_to(self, in_path, out_path, keep_filename=False):
""" Given a filename, resize and save the image per the specification into out_path
:param in_path: path to image file to save. Must be supported by PIL
:param out_path: path to the directory root for the outputted thumbnails to be stored
:return: None
"""
if keep_filename:
filename = path.join(out_path, path.basename(in_path))
else:
filename = path.join(out_path, self.get_thumbnail_name(in_path))
out_path = path.dirname(filename)
if not path.exists(out_path):
os.makedirs(out_path)
if not path.exists(filename):
try:
image = Image.open(in_path)
thumbnail = self.resize(image)
thumbnail.save(filename)
logger.info("Generated Thumbnail {0}".format(path.basename(filename)))
except IOError:
logger.info("Generating Thumbnail for {0} skipped".format(path.basename(filename)))
def resize_thumbnails(pelican):
""" Resize a directory tree full of images into thumbnails
:param pelican: The pelican instance
:return: None
"""
global enabled
if not enabled:
return
in_path = _image_path(pelican)
include_regex = pelican.settings.get('THUMBNAIL_INCLUDE_REGEX')
if include_regex:
pattern = re.compile(include_regex)
is_included = lambda name: pattern.match(name)
else:
is_included = lambda name: not name.startswith('.')
sizes = pelican.settings.get('THUMBNAIL_SIZES', DEFAULT_THUMBNAIL_SIZES)
resizers = dict((k, _resizer(k, v, in_path)) for k,v in sizes.items())
logger.debug("Thumbnailer Started")
for dirpath, _, filenames in os.walk(in_path):
for filename in filenames:
if is_included(filename):
for name, resizer in resizers.items():
in_filename = path.join(dirpath, filename)
out_path = get_out_path(pelican, in_path, in_filename, name)
resizer.resize_file_to(
in_filename,
out_path, pelican.settings.get('THUMBNAIL_KEEP_NAME'))
def get_out_path(pelican, in_path, in_filename, name):
base_out_path = path.join(pelican.settings['OUTPUT_PATH'],
pelican.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR))
logger.debug("Processing thumbnail {0}=>{1}".format(in_filename, name))
if pelican.settings.get('THUMBNAIL_KEEP_NAME', False):
if pelican.settings.get('THUMBNAIL_KEEP_TREE', False):
return path.join(base_out_path, name, path.dirname(path.relpath(in_filename, in_path)))
else:
return path.join(base_out_path, name)
else:
return base_out_path
def _image_path(pelican):
return path.join(pelican.settings['PATH'],
pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR)).rstrip('/')
def expand_gallery(generator, metadata):
""" Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH
:param pelican: The pelican instance
:return: None
"""
if "gallery" not in metadata or metadata['gallery'] is None:
return # If no gallery specified, we do nothing
lines = [ ]
base_path = _image_path(generator)
in_path = path.join(base_path, metadata['gallery'])
template = generator.settings.get('GALLERY_TEMPLATE', DEFAULT_TEMPLATE)
thumbnail_name = generator.settings.get("GALLERY_THUMBNAIL", DEFAULT_GALLERY_THUMB)
thumbnail_prefix = generator.settings.get("")
resizer = _resizer(thumbnail_name, '?x?', base_path)
for dirpath, _, filenames in os.walk(in_path):
for filename in filenames:
if not filename.startswith('.'):
url = path.join(dirpath, filename).replace(base_path, "")[1:]
url = path.join('/static', generator.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR), url).replace('\\', '/')
logger.debug("GALLERY: {0}".format(url))
thumbnail = resizer.get_thumbnail_name(filename)
thumbnail = path.join('/', generator.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR), thumbnail).replace('\\', '/')
lines.append(template.format(
filename=filename,
url=url,
thumbnail=thumbnail,
))
metadata['gallery_content'] = "\n".join(lines)
def register():
signals.finalized.connect(resize_thumbnails)
signals.article_generator_context.connect(expand_gallery)

View File

@ -0,0 +1,56 @@
Summary
-------
This plugin allows easy, variable length summaries directly embedded into the
body of your articles. It introduces two new settings: ``SUMMARY_BEGIN_MARKER``
and ``SUMMARY_END_MARKER``: strings which can be placed directly into an article
to mark the beginning and end of a summary. When found, the standard
``SUMMARY_MAX_LENGTH`` setting will be ignored. The markers themselves will also
be removed from your articles before they are published. The default values
are ``<!-- PELICAN_BEGIN_SUMMARY -->`` and ``<!-- PELICAN_END_SUMMARY -->``.
For example::
Title: My super title
Date: 2010-12-03 10:20
Tags: thats, awesome
Category: yeah
Slug: my-super-post
Author: Alexis Metaireau
This is the content of my super blog post.
<!-- PELICAN_END_SUMMARY -->
and this content occurs after the summary.
Here, the summary is taken to be the first line of the post. Because no
beginning marker was found, it starts at the top of the body. It is possible
to leave out the end marker instead, in which case the summary will start at the
beginning marker and continue to the end of the body.
If no beginning or end marker is found, and if ``SUMMARY_USE_FIRST_PARAGRAPH``
is enabled in the settings, the summary will be the first paragraph of the post.
The plugin also sets a ``has_summary`` attribute on every article. It is True
for articles with an explicitly-defined summary, and False otherwise. (It is
also False for an article truncated by ``SUMMARY_MAX_LENGTH``.) Your templates
can use this e.g. to add a link to the full text at the end of the summary.
reST example
~~~~~~~~~~~~
Inserting the markers into a reStructuredText document makes use of the
comment directive, because raw HTML is automatically escaped. The reST equivalent of the above Markdown example looks like this::
My super title
##############
:date: 2010-12-03 10:20
:tags: thats, awesome
:category: yeah
:slug: my-super-post
:author: Alexis Metaireau
This is the content of my super blog post.
.. PELICAN_END_SUMMARY
and this content occurs after the summary.

View File

@ -0,0 +1 @@
from .summary import *

View File

@ -0,0 +1,105 @@
"""
Summary
-------
This plugin allows easy, variable length summaries directly embedded into the
body of your articles.
"""
from __future__ import unicode_literals
from pelican import signals
from pelican.generators import ArticlesGenerator, StaticGenerator, PagesGenerator
import re
def initialized(pelican):
from pelican.settings import DEFAULT_CONFIG
DEFAULT_CONFIG.setdefault('SUMMARY_BEGIN_MARKER',
'<!-- PELICAN_BEGIN_SUMMARY -->')
DEFAULT_CONFIG.setdefault('SUMMARY_END_MARKER',
'<!-- PELICAN_END_SUMMARY -->')
DEFAULT_CONFIG.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False)
if pelican:
pelican.settings.setdefault('SUMMARY_BEGIN_MARKER',
'<!-- PELICAN_BEGIN_SUMMARY -->')
pelican.settings.setdefault('SUMMARY_END_MARKER',
'<!-- PELICAN_END_SUMMARY -->')
pelican.settings.setdefault('SUMMARY_USE_FIRST_PARAGRAPH', False)
def extract_summary(instance):
# if summary is already specified, use it
# if there is no content, there's nothing to do
if hasattr(instance, '_summary'):
instance.has_summary = True
return
if not instance._content:
instance.has_summary = False
return
begin_marker = instance.settings['SUMMARY_BEGIN_MARKER']
end_marker = instance.settings['SUMMARY_END_MARKER']
use_first_paragraph = instance.settings['SUMMARY_USE_FIRST_PARAGRAPH']
remove_markers = True
content = instance._content
begin_summary = -1
end_summary = -1
if begin_marker:
begin_summary = content.find(begin_marker)
if end_marker:
end_summary = content.find(end_marker)
if begin_summary == -1 and end_summary == -1 and use_first_paragraph:
begin_marker, end_marker = '<p>', '</p>'
remove_markers = False
begin_summary = content.find(begin_marker)
end_summary = content.find(end_marker)
if begin_summary == -1 and end_summary == -1:
instance.has_summary = False
return
# skip over the begin marker, if present
if begin_summary == -1:
begin_summary = 0
else:
begin_summary = begin_summary + len(begin_marker)
if end_summary == -1:
end_summary = None
summary = content[begin_summary:end_summary]
if remove_markers:
# remove the markers from the content
if begin_summary:
content = content.replace(begin_marker, '', 1)
if end_summary:
content = content.replace(end_marker, '', 1)
summary = re.sub(r"<div.*>", "", summary)
summary = re.sub(r"</div>", "", summary)
instance._content = content
instance._summary = summary
instance.has_summary = True
def run_plugin(generators):
for generator in generators:
if isinstance(generator, ArticlesGenerator):
for article in generator.articles:
extract_summary(article)
elif isinstance(generator, PagesGenerator):
for page in generator.pages:
extract_summary(page)
def register():
signals.initialized.connect(initialized)
try:
signals.all_generators_finalized.connect(run_plugin)
except AttributeError:
# NOTE: This results in #314 so shouldn't really be relied on
# https://github.com/getpelican/pelican-plugins/issues/314
signals.content_object_init.connect(extract_summary)

View File

@ -0,0 +1,96 @@
# -*- coding: utf-8 -*-
import unittest
from jinja2.utils import generate_lorem_ipsum
# generate one paragraph, enclosed with <p>
TEST_CONTENT = str(generate_lorem_ipsum(n=1))
TEST_SUMMARY = generate_lorem_ipsum(n=1, html=False)
from pelican.contents import Page
import pelican.settings
import summary
class TestSummary(unittest.TestCase):
def setUp(self):
super(TestSummary, self).setUp()
pelican.settings.DEFAULT_CONFIG['SUMMARY_MAX_LENGTH'] = None
pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = False
summary.register()
summary.initialized(None)
self.page_kwargs = {
'content': TEST_CONTENT,
'context': {
'localsiteurl': '',
},
'metadata': {
'summary': TEST_SUMMARY,
'title': 'foo bar',
'author': 'Blogger',
},
}
def _copy_page_kwargs(self):
# make a deep copy of page_kwargs
page_kwargs = dict([(key, self.page_kwargs[key]) for key in
self.page_kwargs])
for key in page_kwargs:
if not isinstance(page_kwargs[key], dict):
break
page_kwargs[key] = dict([(subkey, page_kwargs[key][subkey])
for subkey in page_kwargs[key]])
return page_kwargs
def test_end_summary(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
page_kwargs['content'] = (
TEST_SUMMARY + '<!-- PELICAN_END_SUMMARY -->' + TEST_CONTENT)
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_SUMMARY)
self.assertEqual(page.content, TEST_SUMMARY + TEST_CONTENT)
def test_begin_summary(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
page_kwargs['content'] = (
'FOOBAR<!-- PELICAN_BEGIN_SUMMARY -->' + TEST_CONTENT)
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_CONTENT)
self.assertEqual(page.content, 'FOOBAR' + TEST_CONTENT)
def test_begin_end_summary(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
page_kwargs['content'] = (
'FOOBAR<!-- PELICAN_BEGIN_SUMMARY -->' + TEST_SUMMARY +
'<!-- PELICAN_END_SUMMARY -->' + TEST_CONTENT)
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_SUMMARY)
self.assertEqual(page.content, 'FOOBAR' + TEST_SUMMARY + TEST_CONTENT)
def test_use_first_paragraph(self):
page_kwargs = self._copy_page_kwargs()
del page_kwargs['metadata']['summary']
pelican.settings.DEFAULT_CONFIG['SUMMARY_USE_FIRST_PARAGRAPH'] = True
page_kwargs['content'] = '<p>' + TEST_SUMMARY + '</p>' + TEST_CONTENT
page = Page(**page_kwargs)
summary.extract_summary(page)
# test both the summary and the marker removal
self.assertEqual(page.summary, TEST_SUMMARY)
self.assertEqual(page.content, '<p>' + TEST_SUMMARY + '</p>' + TEST_CONTENT)
if __name__ == '__main__':
unittest.main()

View File

@ -0,0 +1,33 @@
Thumbnail Creation of images
============================
This plugin creates thumbnails for all of the images found under a specific directory, in various thumbnail sizes.
It requires `PIL` to function properly since `PIL` is used to resize the images, and the thumbnail will only be re-built
if it doesn't already exist (to save processing time).
Installation
-------------
Set up like any other plugin, making sure to set `PLUGIN_PATH` and add `thumbnailer` to the `PLUGINS` list.
[PIL](http://www.pythonware.com/products/pil/) or [Pillow](http://pillow.readthedocs.org/en/latest/installation.html#)
is required. If you choose Pillow, you need to install some additional
[external libraries](http://www.pythonware.com/products/pil/) to add support for image types like `jpg`.
Configuration
-------------
* `IMAGE_PATH` is the path to the image directory. It should reside inside your content directory, and defaults to "pictures".
* `THUMBNAIL_DIR` is the path to the output sub-directory where the thumbnails are generated.
* `THUMBNAIL_SIZES` is a dictionary mapping size name to size specifications.
The generated filename will be `originalname_thumbnailname.ext` unless `THUMBNAIL_KEEP_NAME` is set.
* `THUMBNAIL_KEEP_NAME` is a Boolean that, if set, puts the file with the original name in a thumbnailname folder, named like the key in `THUMBNAIL_SIZES`.
* `THUMBNAIL_KEEP_TREE` is a Boolean that, if set, saves the image directory tree.
* `THUMBNAIL_INCLUDE_REGEX` is an optional string that is used as regular expression to restrict thumbnailing to matching files. By default all files not starting with a dot are respected.
Sizes can be specified using any of the following formats:
* wxh will resize to exactly wxh cropping as necessary to get that size
* wx? will resize so that the width is the specified size, and the height will scale to retain aspect ratio
* ?xh same as wx? but will height being a set size
* s is a shorthand for wxh where w=h

View File

@ -0,0 +1 @@
from .thumbnailer import *

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 KiB

View File

@ -0,0 +1,63 @@
from thumbnailer import _resizer
from unittest import TestCase, main
import os
from PIL import Image
class ThumbnailerTests(TestCase):
def path(self, filename):
return os.path.join(self.img_path, filename)
def setUp(self):
self.img_path = os.path.join(os.path.dirname(__file__), "test_data")
self.img = Image.open(self.path("sample_image.jpg"))
def testSquare(self):
r = _resizer('square', '100', self.img_path)
output = r.resize(self.img)
self.assertEqual((100, 100), output.size)
def testExact(self):
r = _resizer('exact', '250x100', self.img_path)
output = r.resize(self.img)
self.assertEqual((250, 100), output.size)
def testWidth(self):
r = _resizer('aspect', '250x?', self.img_path)
output = r.resize(self.img)
self.assertEqual((250, 166), output.size)
def testHeight(self):
r = _resizer('aspect', '?x250', self.img_path)
output = r.resize(self.img)
self.assertEqual((375, 250), output.size)
class ThumbnailerFilenameTest(TestCase):
def path(self, *parts):
return os.path.join(self.img_path, *parts)
def setUp(self):
self.img_path = os.path.join(os.path.dirname(__file__), "test_data")
def testRoot(self):
"""Test a file that is in the root of img_path."""
r = _resizer('square', '100', self.img_path)
new_name = r.get_thumbnail_name(self.path('sample_image.jpg'))
self.assertEqual('sample_image_square.jpg', new_name)
def testRootWithSlash(self):
r = _resizer('square', '100', self.img_path + '/')
new_name = r.get_thumbnail_name(self.path('sample_image.jpg'))
self.assertEqual('sample_image_square.jpg', new_name)
def testSubdir(self):
"""Test a file that is in a sub-directory of img_path."""
r = _resizer('square', '100', self.img_path)
new_name = r.get_thumbnail_name(self.path('subdir', 'sample_image.jpg'))
self.assertEqual('subdir/sample_image_square.jpg', new_name)
if __name__=="__main__":
main()

View File

@ -0,0 +1,208 @@
import os
import os.path as path
import re
from pelican import signals
import logging
logger = logging.getLogger(__name__)
try:
from PIL import Image, ImageOps
enabled = True
except ImportError:
logging.warning("Unable to load PIL, disabling thumbnailer")
enabled = False
DEFAULT_IMAGE_DIR = "pictures"
DEFAULT_THUMBNAIL_DIR = "thumbnails"
DEFAULT_THUMBNAIL_SIZES = {
'thumbnail_square': '150',
'thumbnail_wide': '150x?',
'thumbnail_tall': '?x150',
}
DEFAULT_TEMPLATE = """<a href="{url}" rel="shadowbox" title="{filename}"><img src="{thumbnail}" alt="{filename}"></a>"""
DEFAULT_GALLERY_THUMB = "thumbnail_square"
class _resizer(object):
""" Resizes based on a text specification, see readme """
REGEX = re.compile(r'(\d+|\?)x(\d+|\?)')
def __init__(self, name, spec, root):
self._name = name
self._spec = spec
# The location of input images from _image_path.
self._root = root
def _null_resize(self, w, h, image):
return image
def _exact_resize(self, w, h, image):
retval = ImageOps.fit(image, (w,h), Image.BICUBIC)
return retval
def _aspect_resize(self, w, h, image):
retval = image.copy()
retval.thumbnail((w, h), Image.ANTIALIAS)
return retval
def resize(self, image):
resizer = self._null_resize
# Square resize and crop
if 'x' not in self._spec:
resizer = self._exact_resize
targetw = int(self._spec)
targeth = targetw
else:
matches = self.REGEX.search(self._spec)
tmpw = matches.group(1)
tmph = matches.group(2)
# Full Size
if tmpw == '?' and tmph == '?':
targetw = image.size[0]
targeth = image.size[1]
resizer = self._null_resize
# Set Height Size
if tmpw == '?':
targetw = image.size[0]
targeth = int(tmph)
resizer = self._aspect_resize
# Set Width Size
elif tmph == '?':
targetw = int(tmpw)
targeth = image.size[1]
resizer = self._aspect_resize
# Scale and Crop
else:
targetw = int(tmpw)
targeth = int(tmph)
resizer = self._exact_resize
logging.debug("Using resizer {0}".format(resizer.__name__))
return resizer(targetw, targeth, image)
def get_thumbnail_name(self, in_path):
# Find the partial path + filename beyond the input image directory.
prefix = path.commonprefix([in_path, self._root])
new_filename = in_path[len(prefix) + 1:]
# Generate the new filename.
(basename, ext) = path.splitext(new_filename)
return "{0}_{1}{2}".format(basename, self._name, ext)
def resize_file_to(self, in_path, out_path, keep_filename=False):
""" Given a filename, resize and save the image per the specification into out_path
:param in_path: path to image file to save. Must be supported by PIL
:param out_path: path to the directory root for the outputted thumbnails to be stored
:return: None
"""
if keep_filename:
filename = path.join(out_path, path.basename(in_path))
else:
filename = path.join(out_path, self.get_thumbnail_name(in_path))
out_path = path.dirname(filename)
if not path.exists(out_path):
os.makedirs(out_path)
if not path.exists(filename):
try:
image = Image.open(in_path)
thumbnail = self.resize(image)
thumbnail.save(filename)
logger.info("Generated Thumbnail {0}".format(path.basename(filename)))
except IOError:
logger.info("Generating Thumbnail for {0} skipped".format(path.basename(filename)))
def resize_thumbnails(pelican):
""" Resize a directory tree full of images into thumbnails
:param pelican: The pelican instance
:return: None
"""
global enabled
if not enabled:
return
in_path = _image_path(pelican)
include_regex = pelican.settings.get('THUMBNAIL_INCLUDE_REGEX')
if include_regex:
pattern = re.compile(include_regex)
is_included = lambda name: pattern.match(name)
else:
is_included = lambda name: not name.startswith('.')
sizes = pelican.settings.get('THUMBNAIL_SIZES', DEFAULT_THUMBNAIL_SIZES)
resizers = dict((k, _resizer(k, v, in_path)) for k,v in sizes.items())
logger.debug("Thumbnailer Started")
for dirpath, _, filenames in os.walk(in_path):
for filename in filenames:
if is_included(filename):
for name, resizer in resizers.items():
in_filename = path.join(dirpath, filename)
out_path = get_out_path(pelican, in_path, in_filename, name)
resizer.resize_file_to(
in_filename,
out_path, pelican.settings.get('THUMBNAIL_KEEP_NAME'))
def get_out_path(pelican, in_path, in_filename, name):
base_out_path = path.join(pelican.settings['OUTPUT_PATH'],
pelican.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR))
logger.debug("Processing thumbnail {0}=>{1}".format(in_filename, name))
if pelican.settings.get('THUMBNAIL_KEEP_NAME', False):
if pelican.settings.get('THUMBNAIL_KEEP_TREE', False):
return path.join(base_out_path, name, path.dirname(path.relpath(in_filename, in_path)))
else:
return path.join(base_out_path, name)
else:
return base_out_path
def _image_path(pelican):
return path.join(pelican.settings['PATH'],
pelican.settings.get("IMAGE_PATH", DEFAULT_IMAGE_DIR)).rstrip('/')
def expand_gallery(generator, metadata):
""" Expand a gallery tag to include all of the files in a specific directory under IMAGE_PATH
:param pelican: The pelican instance
:return: None
"""
if "gallery" not in metadata or metadata['gallery'] is None:
return # If no gallery specified, we do nothing
lines = [ ]
base_path = _image_path(generator)
in_path = path.join(base_path, metadata['gallery'])
template = generator.settings.get('GALLERY_TEMPLATE', DEFAULT_TEMPLATE)
thumbnail_name = generator.settings.get("GALLERY_THUMBNAIL", DEFAULT_GALLERY_THUMB)
thumbnail_prefix = generator.settings.get("")
resizer = _resizer(thumbnail_name, '?x?', base_path)
for dirpath, _, filenames in os.walk(in_path):
for filename in filenames:
if not filename.startswith('.'):
url = path.join(dirpath, filename).replace(base_path, "")[1:]
url = path.join('/static', generator.settings.get('IMAGE_PATH', DEFAULT_IMAGE_DIR), url).replace('\\', '/')
logger.debug("GALLERY: {0}".format(url))
thumbnail = resizer.get_thumbnail_name(filename)
thumbnail = path.join('/', generator.settings.get('THUMBNAIL_DIR', DEFAULT_THUMBNAIL_DIR), thumbnail).replace('\\', '/')
lines.append(template.format(
filename=filename,
url=url,
thumbnail=thumbnail,
))
metadata['gallery_content'] = "\n".join(lines)
def register():
signals.finalized.connect(resize_thumbnails)
signals.article_generator_context.connect(expand_gallery)