--- This plugin adds `width` and `height` tags to `img` when they are not present. It also adds them when only one of them is present (respecting proportions).main
| @@ -0,0 +1,141 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| Contains classes to handle images related things | |||||
| """ | |||||
| from hyde.plugin import Plugin | |||||
| import re | |||||
| import Image | |||||
| class ImageSizerPlugin(Plugin): | |||||
| """ | |||||
| Each HTML page is modified to add width and height for images if | |||||
| they are not already specified. | |||||
| """ | |||||
| def __init__(self, site): | |||||
| super(ImageSizerPlugin, self).__init__(site) | |||||
| self.cache = {} | |||||
| def _handle_img(self, resource, src, width, height): | |||||
| """Determine what should be added to an img tag""" | |||||
| if height is not None and width is not None: | |||||
| return "" # Nothing | |||||
| if src is None: | |||||
| self.logger.warn("[%s] has an img tag without src attribute" % resource) | |||||
| return "" # Nothing | |||||
| if src not in self.cache: | |||||
| if not re.match(r"(/[^/]|[^/]).*", src): | |||||
| # Not a local link | |||||
| return "" # Nothing | |||||
| if src.startswith("/"): | |||||
| # Absolute resource | |||||
| path = src.lstrip("/") | |||||
| image = self.site.content.resource_from_relative_deploy_path(path) | |||||
| else: | |||||
| # Relative resource | |||||
| path = resource.node.source_folder.child(src) | |||||
| image = self.site.content.resource_from_path(path) | |||||
| if image is None: | |||||
| self.logger.warn( | |||||
| "[%s] has an unknown image" % resource) | |||||
| return "" # Nothing | |||||
| if image.source_file.kind not in ['png', 'jpg', 'jpeg', 'gif']: | |||||
| self.logger.warn( | |||||
| "[%s] has an img tag not linking to an image" % resource) | |||||
| return "" # Nothing | |||||
| # Now, get the size of the image | |||||
| try: | |||||
| self.cache[src] = Image.open(image.path).size | |||||
| except IOError: | |||||
| self.logger.warn( | |||||
| "Unable to process image [%s]" % image) | |||||
| self.cache[src] = (None, None) | |||||
| return "" # Nothing | |||||
| self.logger.debug("Image [%s] is %s" % (src, | |||||
| self.cache[src])) | |||||
| new_width, new_height = self.cache[src] | |||||
| if new_width is None or new_height is None: | |||||
| return "" # Nothing | |||||
| if width is not None: | |||||
| return 'height="%d" ' % (int(width)*new_height/new_width) | |||||
| elif height is not None: | |||||
| return 'width="%d" ' % (int(height)*new_width/new_height) | |||||
| return 'height="%d" width="%d" ' % (new_height, new_width) | |||||
| def text_resource_complete(self, resource, text): | |||||
| """ | |||||
| When the resource is generated, search for img tag and specify | |||||
| their sizes. | |||||
| Some img tags may be missed, this is not a perfect parser. | |||||
| """ | |||||
| try: | |||||
| mode = self.site.config.mode | |||||
| except AttributeError: | |||||
| mode = "production" | |||||
| if not resource.source_file.kind == 'html': | |||||
| return | |||||
| if mode.startswith('dev'): | |||||
| self.logger.debug("Skipping sizer in development mode.") | |||||
| return | |||||
| pos = 0 # Position in text | |||||
| img = None # Position of current img tag | |||||
| state = "find-img" | |||||
| while pos < len(text): | |||||
| if state == "find-img": | |||||
| img = text.find("<img", pos) | |||||
| if img == -1: | |||||
| break # No more img tag | |||||
| pos = img + len("<img") | |||||
| if not text[pos].isspace(): | |||||
| continue # Not an img tag | |||||
| pos = pos + 1 | |||||
| tags = {"src": "", | |||||
| "width": "", | |||||
| "height": ""} | |||||
| state = "find-attr" | |||||
| continue | |||||
| if state == "find-attr": | |||||
| if text[pos] == ">": | |||||
| # We get our img tag | |||||
| insert = self._handle_img(resource, | |||||
| tags["src"] or None, | |||||
| tags["width"] or None, | |||||
| tags["height"] or None) | |||||
| img = img + len("<img ") | |||||
| text = "".join([text[:img], insert, text[img:]]) | |||||
| state = "find-img" | |||||
| pos = pos + 1 | |||||
| continue | |||||
| attr = None | |||||
| for tag in tags: | |||||
| if text[pos:(pos+len(tag)+1)] == ("%s=" % tag): | |||||
| attr = tag | |||||
| pos = pos + len(tag) + 1 | |||||
| break | |||||
| if not attr: | |||||
| pos = pos + 1 | |||||
| continue | |||||
| if text[pos] in ["'", '"']: | |||||
| pos = pos + 1 | |||||
| state = "get-value" | |||||
| continue | |||||
| if state == "get-value": | |||||
| if text[pos] == ">": | |||||
| state = "find-attr" | |||||
| continue | |||||
| if text[pos] in ["'", '"'] or text[pos].isspace(): | |||||
| # We got our value | |||||
| pos = pos + 1 | |||||
| state = "find-attr" | |||||
| continue | |||||
| tags[attr] = tags[attr] + text[pos] | |||||
| pos = pos + 1 | |||||
| continue | |||||
| return text | |||||
| @@ -0,0 +1,124 @@ | |||||
| # -*- coding: utf-8 -*- | |||||
| """ | |||||
| Use nose | |||||
| `$ pip install nose` | |||||
| `$ nosetests` | |||||
| """ | |||||
| from hyde.fs import File, Folder | |||||
| from hyde.generator import Generator | |||||
| from hyde.site import Site | |||||
| from pyquery import PyQuery | |||||
| TEST_SITE = File(__file__).parent.parent.child_folder('_test') | |||||
| IMAGE_SOURCE = File(__file__).parent.child_folder('optipng') | |||||
| IMAGE_NAME = "hyde-lt-b.png" | |||||
| IMAGE_SIZE = (538, 132) | |||||
| class TestImageSizer(object): | |||||
| def setUp(self): | |||||
| TEST_SITE.make() | |||||
| TEST_SITE.parent.child_folder( | |||||
| 'sites/test_jinja').copy_contents_to(TEST_SITE) | |||||
| IMAGES = TEST_SITE.child_folder('content/media/img') | |||||
| IMAGES.make() | |||||
| IMAGE_SOURCE.copy_contents_to(IMAGES) | |||||
| def tearDown(self): | |||||
| TEST_SITE.delete() | |||||
| def _generic_test_image(self, text): | |||||
| site = Site(TEST_SITE) | |||||
| site.config.mode = "production" | |||||
| site.config.plugins = ['hyde.ext.plugins.images.ImageSizerPlugin'] | |||||
| tlink = File(site.content.source_folder.child('timg.html')) | |||||
| tlink.write(text) | |||||
| gen = Generator(site) | |||||
| gen.generate_all() | |||||
| f = File(site.config.deploy_root_path.child(tlink.name)) | |||||
| assert f.exists | |||||
| html = f.read_all() | |||||
| assert html | |||||
| print html | |||||
| return html | |||||
| def test_size_image(self): | |||||
| text = u""" | |||||
| <img src="/media/img/%s"> | |||||
| """ % IMAGE_NAME | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] in html | |||||
| def test_size_image_relative(self): | |||||
| text = u""" | |||||
| <img src="media/img/%s"> | |||||
| """ % IMAGE_NAME | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] in html | |||||
| def test_size_image_no_resize(self): | |||||
| text = u""" | |||||
| <img src="/media/img/%s" width="2000" height="150"> | |||||
| """ % IMAGE_NAME | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] not in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] not in html | |||||
| def test_size_image_size_proportional(self): | |||||
| text = u""" | |||||
| <img src="/media/img/%s" width="%d"> | |||||
| """ % (IMAGE_NAME, IMAGE_SIZE[0]*2) | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % (IMAGE_SIZE[0]*2) in html | |||||
| assert ' height="%d"' % (IMAGE_SIZE[1]*2) in html | |||||
| def test_size_image_not_exists(self): | |||||
| text = u""" | |||||
| <img src="/media/img/hyde-logo-no.png"> | |||||
| """ | |||||
| html = self._generic_test_image(text) | |||||
| def test_size_image_multiline(self): | |||||
| text = u""" | |||||
| <img | |||||
| src="/media/img/%s" | |||||
| > | |||||
| """ % IMAGE_NAME | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] in html | |||||
| def test_size_multiple_images(self): | |||||
| text = u""" | |||||
| <img src="/media/img/%s"> | |||||
| <img src="/media/img/%s">Hello <img src="/media/img/%s"> | |||||
| <img src="/media/img/%s">Bye | |||||
| """ % ((IMAGE_NAME,)*4) | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] in html | |||||
| assert 'Hello ' in html | |||||
| assert 'Bye' in html | |||||
| assert len([f for f in html.split("<img") | |||||
| if ' width=' in f]) == 4 | |||||
| assert len([f for f in html.split("<img") | |||||
| if ' height=' in f]) == 4 | |||||
| def test_size_malformed1(self): | |||||
| text = u""" | |||||
| <img src="/media/img/%s> | |||||
| """ % IMAGE_NAME | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] in html | |||||
| def test_size_malformed2(self): | |||||
| text = u""" | |||||
| <img src="/media/img/%s alt="hello"> | |||||
| """ % IMAGE_NAME | |||||
| html = self._generic_test_image(text) | |||||
| assert ' width="%d"' % IMAGE_SIZE[0] in html | |||||
| assert ' height="%d"' % IMAGE_SIZE[1] in html | |||||