From 174559ee9eaf2f4148ecc94599d5655d160a5098 Mon Sep 17 00:00:00 2001 From: Brian Mattern Date: Mon, 26 Mar 2012 16:37:25 -0700 Subject: [PATCH] tests for thumbnailer plugin + larger/smaller options remove debug statements rename thumb_size -> thumb_scale_size --- hyde/ext/plugins/images.py | 87 ++++++++-- hyde/tests/ext/images/landscape.jpg | Bin 0 -> 377 bytes hyde/tests/ext/images/portrait.jpg | Bin 0 -> 377 bytes hyde/tests/ext/test_images.py | 246 ++++++++++++++++++++++++---- 4 files changed, 287 insertions(+), 46 deletions(-) create mode 100644 hyde/tests/ext/images/landscape.jpg create mode 100644 hyde/tests/ext/images/portrait.jpg diff --git a/hyde/ext/plugins/images.py b/hyde/ext/plugins/images.py index cfeca5e..cc3ffd4 100644 --- a/hyde/ext/plugins/images.py +++ b/hyde/ext/plugins/images.py @@ -149,6 +149,44 @@ class ImageSizerPlugin(Plugin): return text +def scale_aspect(a, b1, b2): + from math import ceil + """ + Scales a by b2/b1 rounding up to nearest integer + """ + return int(ceil(a * b2 / float(b1))) + + +def thumb_scale_size(orig_width, orig_height, dim1, dim2, preserve_orientation=False): + """ + Determine thumbnail size + + Params: + orig_width, orig_height: original image dimensions + dim1, dim2: thumbnail dimensions + preserve_orientatin: whether to preserve original image's orientation + + If `preserve_orientation` is True and the original image is portrait, then + dim1 corresponds to the height and dim2 corresponds to the width + Otherwise, dim1 is width and dim2 is height. + """ + if preserve_orientation and orig_height > orig_width: + width, height = dim2, dim1 + else: + width, height = dim1, dim2 + + if width is None: + width = scale_aspect(orig_width, orig_height, height) + elif height is None: + height = scale_aspect(orig_height, orig_width, width) + elif orig_width*height >= orig_height*width: + width = scale_aspect(orig_width, orig_height, height) + else: + height = scale_aspect(orig_height, orig_width, width) + + return width, height + + class ImageThumbnailsPlugin(Plugin): """ Provide a function to get thumbnail for any image resource. @@ -172,20 +210,26 @@ class ImageThumbnailsPlugin(Plugin): include: - '*.png' - '*.jpg' - which means - make from every picture two thumbnails with different prefixes + - lfrom every picture arger: 100 + prefix: thumbs3_ + include: + - '*.jpg' + which means - make three thumbnails from every picture with different prefixes and sizes If both width and height defined, image would be cropped, you can define crop_type as one of these values: "topleft", "center" and "bottomright". "topleft" is default. + XXX fix docs for larger/smaller + Currently, only supports PNG and JPG. """ def __init__(self, site): super(ImageThumbnailsPlugin, self).__init__(site) - def thumb(self, resource, width, height, prefix, crop_type): + def thumb(self, resource, width, height, prefix, crop_type, preserve_orientation=False): """ Generate a thumbnail for the given image """ @@ -211,17 +255,13 @@ class ImageThumbnailsPlugin(Plugin): im = Image.open(resource.path) if im.mode != 'RGBA': im = im.convert('RGBA') - resize_width = width - resize_height = height - if resize_width is None: - resize_width = im.size[0]*height/im.size[1] + 1 - elif resize_height is None: - resize_height = im.size[1]*width/im.size[0] + 1 - elif im.size[0]*height >= im.size[1]*width: - resize_width = im.size[0]*height/im.size[1] - else: - resize_height = im.size[1]*width/im.size[0] + resize_width, resize_height = thumb_scale_size(im.size[0], im.size[1], width, height, preserve_orientation) + + if preserve_orientation and im.size[1] > im.size[0]: + width, height = height, width + + self.logger.debug("Resize to: %d,%d" % (resize_width, resize_height)) im = im.resize((resize_width, resize_height), Image.ANTIALIAS) if width is not None and height is not None: shiftx = shifty = 0 @@ -247,6 +287,8 @@ class ImageThumbnailsPlugin(Plugin): config = self.site.config defaults = { "width": None, "height": None, + "larger": None, + "smaller": None, "crop_type": "topleft", "prefix": 'thumb_'} if hasattr(config, 'thumbnails'): @@ -262,17 +304,32 @@ class ImageThumbnailsPlugin(Plugin): prefix = th.prefix if hasattr(th, 'prefix') else defaults['prefix'] height = th.height if hasattr(th, 'height') else defaults['height'] width = th.width if hasattr(th, 'width') else defaults['width'] + larger = th.larger if hasattr(th, 'larger') else defaults['larger'] + smaller = th.smaller if hasattr(th, 'smaller') else defaults['smaller'] crop_type = th.crop_type if hasattr(th, 'crop_type') else defaults['crop_type'] if crop_type not in ["topleft", "center", "bottomright"]: self.logger.error("Unknown crop_type defined for node [%s]" % node) continue - if width is None and height is None: - self.logger.error("Both width and height are not set for node [%s]" % node) + if width is None and height is None and larger is None and smaller is None: + self.logger.error("At least one of width, height, larger, or smaller must be set for node [%s]" % node) + continue + + if ((larger is not None or smaller is not None) and + (width is not None or height is not None)): + self.logger.error("It is not valid to specify both one of width/height and one of larger/smaller for node [%s]" % node) continue + + if larger is None and smaller is None: + preserve_orientation = False + dim1, dim2 = width, height + else: + preserve_orientation = True + dim1, dim2 = larger, smaller + thumbs_list = [] for inc in include: for path in glob.glob(node.path + os.sep + inc): thumbs_list.append(path) for resource in node.resources: if resource.source_file.kind in ["jpg", "png"] and resource.path in thumbs_list: - self.thumb(resource, width, height, prefix, crop_type) + self.thumb(resource, dim1, dim2, prefix, crop_type, preserve_orientation) diff --git a/hyde/tests/ext/images/landscape.jpg b/hyde/tests/ext/images/landscape.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ca0cdc5bb8133af51563b5570c41ac23bcd3d610 GIT binary patch literal 377 zcmex=LJ%Z3brsR%R9! z7G_o;!OF_Y#?HgR4g~z%+?+gu{6a#4{DOkQVlv{wB2uD)f)a`nQnIr0^76vsN-9cn zDl&5Nav(z(fm+$w*!eg(_~b+cMdU~Z{|_(-axg?OR4_9tF)#@-G7B>PKf)jibUyLJ%Z3brsR%R9! z7G_o;!OF_Y#?HgR4g~z%+?+gu{6a#4{DOkQVlv{wB2uD)f)a`nQnIr0^76vsN-9cn zDl&5Nav(z(fm+$w*!eg(_~b+cMdU~Z{|_(-axhddL@_feF)#@-G7B>PKf)jibUy -""" % IMAGE_NAME +""" % PORTRAIT_IMAGE html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] in html - assert ' height="%d"' % IMAGE_SIZE[1] in html + assert ' width="%d"' % PORTRAIT_SIZE[0] in html + assert ' height="%d"' % PORTRAIT_SIZE[1] in html def test_size_image_relative(self): text = u""" -""" % IMAGE_NAME +""" % PORTRAIT_IMAGE html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] in html - assert ' height="%d"' % IMAGE_SIZE[1] in html + assert ' width="%d"' % PORTRAIT_SIZE[0] in html + assert ' height="%d"' % PORTRAIT_SIZE[1] in html def test_size_image_no_resize(self): text = u""" -""" % IMAGE_NAME +""" % PORTRAIT_IMAGE html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] not in html - assert ' height="%d"' % IMAGE_SIZE[1] not in html + assert ' width="%d"' % PORTRAIT_SIZE[0] not in html + assert ' height="%d"' % PORTRAIT_SIZE[1] not in html def test_size_image_size_proportional(self): text = u""" -""" % (IMAGE_NAME, IMAGE_SIZE[0]*2) +""" % (PORTRAIT_IMAGE, PORTRAIT_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 + assert ' width="%d"' % (PORTRAIT_SIZE[0]*2) in html + assert ' height="%d"' % (PORTRAIT_SIZE[1]*2) in html def test_size_image_not_exists(self): text = u""" @@ -87,21 +95,22 @@ class TestImageSizer(object): def test_size_image_multiline(self): text = u""" - -""" % IMAGE_NAME + +""" % PORTRAIT_IMAGE html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] in html - assert ' height="%d"' % IMAGE_SIZE[1] in html + assert ' width="%d"' % PORTRAIT_SIZE[0] in html + assert ' height="%d"' % PORTRAIT_SIZE[1] in html def test_size_multiple_images(self): text = u""" Hello Bye -""" % ((IMAGE_NAME,)*4) +""" % ((PORTRAIT_IMAGE,)*4) html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] in html - assert ' height="%d"' % IMAGE_SIZE[1] in html + assert ' width="%d"' % PORTRAIT_SIZE[0] in html + assert ' height="%d"' % PORTRAIT_SIZE[1] in html assert 'Hello ' in html assert 'Bye' in html assert len([f for f in html.split(" -""" % IMAGE_NAME +""" % PORTRAIT_IMAGE html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] in html - assert ' height="%d"' % IMAGE_SIZE[1] in html + assert ' width="%d"' % PORTRAIT_SIZE[0] in html + assert ' height="%d"' % PORTRAIT_SIZE[1] in html def test_outside_media_url(self): self.site.config.media_url = "http://media.example.com/" text = u""" hello -""" % IMAGE_NAME +""" % PORTRAIT_IMAGE html = self._generic_test_image(text) - assert ' width="%d"' % IMAGE_SIZE[0] in html - assert ' height="%d"' % IMAGE_SIZE[1] in html + assert ' width="%d"' % PORTRAIT_SIZE[0] in html + assert ' height="%d"' % PORTRAIT_SIZE[1] in html + +class TestImageThumbSize(object): + + def test_width_only(self): + ow, oh = 100, 200 + nw, nh = thumb_scale_size(ow, oh, 50, None) + assert nw == 50 + assert nh == 100 + + def test_width_only_nonintegral(self): + ow, oh = 100, 205 + nw, nh = thumb_scale_size(ow, oh, 50, None) + assert nw == 50 + assert nh == 103 + + def test_height_only(self): + ow, oh = 100, 200 + nw, nh = thumb_scale_size(ow, oh, None, 100) + assert nw == 50 + assert nh == 100 + + def test_height_only_nonintegral(self): + ow, oh = 105, 200 + nw, nh = thumb_scale_size(ow, oh, None, 100) + assert nw == 53 + assert nh == 100 + + def test_height_and_width_portrait(self): + ow, oh = 100, 200 + nw, nh = thumb_scale_size(ow, oh, 50, 50) + assert nw == 50 + assert nh == 100 + + def test_height_and_width_landscape(self): + ow, oh = 200, 100 + nw, nh = thumb_scale_size(ow, oh, 50, 50) + assert nw == 100 + assert nh == 50 + + def test_larger_only_portrait(self): + ow, oh = 100, 200 + nw, nh = thumb_scale_size(ow, oh, 50, None, True) + assert nw == 25 + assert nh == 50 + + def test_larger_only_landscape(self): + ow, oh = 200, 100 + nw, nh = thumb_scale_size(ow, oh, 50, None, True) + assert nw == 50 + assert nh == 25 + + def test_smaller_only_portrait(self): + ow, oh = 100, 200 + nw, nh = thumb_scale_size(ow, oh, None, 50, True) + assert nw == 50 + assert nh == 100 + + def test_smaller_only_landscape(self): + ow, oh = 200, 100 + nw, nh = thumb_scale_size(ow, oh, None, 50, True) + assert nw == 100 + assert nh == 50 + + def test_larger_and_smaller_portrait(self): + ow, oh = 100, 200 + nw, nh = thumb_scale_size(ow, oh, 100, 50, True) + assert nw == 50 + assert nh == 100 + + def test_larger_and_smaller_landscape(self): + ow, oh = 200, 100 + nw, nh = thumb_scale_size(ow, oh, 100, 50, True) + assert nw == 100 + assert nh == 50 + +class TestImageThumbnails(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) + self.image_folder = IMAGES + self.site = Site(TEST_SITE) + + def tearDown(self): + TEST_SITE.delete() + + def _generate_site_with_meta(self, meta): + self.site.config.mode = "production" + self.site.config.plugins = ['hyde.ext.plugins.meta.MetaPlugin', 'hyde.ext.plugins.images.ImageThumbnailsPlugin'] + + mlink = File(self.image_folder.child('meta.yaml')) + meta_text = yaml.dump(meta, default_flow_style=False) + mlink.write(meta_text) + gen = Generator(self.site) + gen.generate_all() + + def _test_generic_thumbnails(self, meta): + self._generate_site_with_meta(meta) + thumb_meta = meta.get('thumbnails', []) + for th in thumb_meta: + prefix = th.get('prefix') + if prefix is None: + continue + + for fn in [PORTRAIT_IMAGE, LANDSCAPE_IMAGE]: + f = File(self._deployed_image(prefix, fn)) + assert f.exists + + def _deployed_image(self, prefix, filename): + return self.site.config.deploy_root_path.child('media/img/%s%s'%(prefix,filename)) + + def test_width(self): + prefix='thumb_' + meta = dict(thumbnails=[dict(width=50, prefix=prefix, include=['*.jpg'])]) + self._test_generic_thumbnails(meta) + for fn in IMAGES: + im = Image.open(self._deployed_image(prefix, fn)) + assert im.size[0] == 50 + + def test_height(self): + prefix='thumb_' + meta = dict(thumbnails=[dict(height=50, prefix=prefix, include=['*.jpg'])]) + self._test_generic_thumbnails(meta) + for fn in IMAGES: + im = Image.open(self._deployed_image(prefix, fn)) + assert im.size[1] == 50 + + def test_width_and_height(self): + prefix='thumb_' + meta = dict(thumbnails=[dict(width=50, height=50, prefix=prefix, include=['*.jpg'])]) + self._test_generic_thumbnails(meta) + for fn in IMAGES: + im = Image.open(self._deployed_image(prefix, fn)) + assert im.size[0] == 50 + assert im.size[1] == 50 + + def test_larger(self): + prefix='thumb_' + meta = dict(thumbnails=[dict(larger=50, prefix=prefix, include=['*.jpg'])]) + self._test_generic_thumbnails(meta) + + im = Image.open(self._deployed_image(prefix, PORTRAIT_IMAGE)) + assert im.size[1] == 50 + + im = Image.open(self._deployed_image(prefix, LANDSCAPE_IMAGE)) + assert im.size[0] == 50 + + def test_smaller(self): + prefix='thumb_' + meta = dict(thumbnails=[dict(smaller=50, prefix=prefix, include=['*.jpg'])]) + self._test_generic_thumbnails(meta) + + im = Image.open(self._deployed_image(prefix, PORTRAIT_IMAGE)) + assert im.size[0] == 50 + + im = Image.open(self._deployed_image(prefix, LANDSCAPE_IMAGE)) + assert im.size[1] == 50 + + + def test_larger_and_smaller(self): + prefix='thumb_' + meta = dict(thumbnails=[dict(larger=100, smaller=50, prefix=prefix, include=['*.jpg'])]) + self._test_generic_thumbnails(meta) + + im = Image.open(self._deployed_image(prefix, PORTRAIT_IMAGE)) + assert im.size[0] == 50 + assert im.size[1] == 100 + + im = Image.open(self._deployed_image(prefix, LANDSCAPE_IMAGE)) + assert im.size[0] == 100 + assert im.size[1] == 50