"""Smaller copies of an uploaded image: which widths exist, and what refuses. Disposable temp directory for the upload root; Pillow does the real work, so these are real images in and real WebP out rather than mocks agreeing with themselves. """ import io import os import tempfile import unittest os.environ["DATABASE_URL"] = "sqlite:///:memory:" from PIL import Image from app.config import settings from app.services import storage_service, thumbnails def png(width, height, colour=(200, 30, 40)): buffer = io.BytesIO() Image.new("RGB", (width, height), colour).save(buffer, format="PNG") return buffer.getvalue() class ThumbnailTests(unittest.TestCase): def setUp(self): self.root = tempfile.TemporaryDirectory() self.previous = (settings.UPLOAD_DIR, settings.STORAGE_BACKEND) settings.UPLOAD_DIR = self.root.name # Pinned local, so the test is about resizing rather than about which # bucket this container happens to be pointed at. settings.STORAGE_BACKEND = "local" def tearDown(self): settings.UPLOAD_DIR, settings.STORAGE_BACKEND = self.previous self.root.cleanup() def put(self, key, data): storage_service.save(key, data) return key def test_a_wide_image_comes_back_narrower_and_as_webp(self): key = self.put("questions/big.png", png(1600, 900)) small = thumbnails.get(key, 256) self.assertIsNotNone(small) image = Image.open(io.BytesIO(small)) self.assertEqual(image.width, 256) self.assertEqual(image.format, "WEBP") # The aspect ratio is kept rather than the height being guessed. self.assertEqual(image.height, round(900 * 256 / 1600)) self.assertLess(len(small), len(storage_service.load(key))) def test_it_is_made_once_and_kept_beside_the_original(self): key = self.put("questions/kept.png", png(1200, 600)) first = thumbnails.get(key, 640) self.assertIsNotNone(first) stored = storage_service.load(thumbnails.thumb_key(key, 640)) self.assertEqual(stored, first) # Removing the original must not change what a stored derivative # returns: the second reader is served from the copy, not from a resize. storage_service.delete(key) self.assertEqual(thumbnails.get(key, 640), first) def test_a_small_image_is_never_enlarged(self): key = self.put("questions/small.png", png(180, 120)) # None means "there is nothing smaller worth serving", and the caller # sends the original. Scaling up invents detail and costs bytes to do it. self.assertIsNone(thumbnails.get(key, 256)) self.assertIsNone(thumbnails.get(key, 640)) def test_only_the_two_widths_exist(self): key = self.put("questions/widths.png", png(1600, 900)) self.assertEqual(thumbnails.WIDTHS, (256, 640)) for width in (1, 255, 512, 641, 4000): self.assertIsNone(thumbnails.get(key, width)) def test_what_is_not_an_image_is_left_alone(self): self.assertFalse(thumbnails.is_resizable("documents/handbook.pdf")) self.assertFalse(thumbnails.is_resizable("figures/diagram.svg")) key = self.put("documents/handbook.pdf", b"%PDF-1.4 not really") self.assertIsNone(thumbnails.get(key, 256)) def test_a_file_that_is_not_the_image_it_claims_is_not_an_error(self): key = self.put("questions/broken.png", b"this is not a PNG") self.assertIsNone(thumbnails.get(key, 256)) def test_forgetting_removes_every_derivative(self): key = self.put("questions/gone.png", png(1600, 900)) for width in thumbnails.WIDTHS: self.assertIsNotNone(thumbnails.get(key, width)) thumbnails.forget(key) for width in thumbnails.WIDTHS: self.assertIsNone(storage_service.load(thumbnails.thumb_key(key, width))) def test_a_sideways_photograph_is_turned_the_right_way_up(self): # Orientation 6 is "rotate 90° clockwise to view", which is how every # phone stores a portrait photograph. A thumbnail made without reading # it is a sideways thumbnail. buffer = io.BytesIO() image = Image.new("RGB", (1200, 600), (10, 20, 30)) exif = image.getexif() exif[274] = 6 image.save(buffer, format="JPEG", exif=exif) key = self.put("questions/sideways.jpg", buffer.getvalue()) small = Image.open(io.BytesIO(thumbnails.get(key, 256))) self.assertEqual(small.width, 256) self.assertGreater(small.height, small.width) if __name__ == "__main__": unittest.main()