diff options
| author | neobooru <50623835+neobooru@users.noreply.github.com> | 2021-06-01 16:57:29 +0200 |
|---|---|---|
| committer | neobooru <50623835+neobooru@users.noreply.github.com> | 2021-06-01 16:57:29 +0200 |
| commit | 5ea9e27e483a01a5814d682134e753aedeaec305 (patch) | |
| tree | f0e04646e967f0209bdbf008966dc941f00de3be /server/szurubooru | |
| parent | 027e83a7e74d227f9552a52ea7a4b8ee6de989f4 (diff) | |
| parent | 2949431d9a1b662dcbec68beb178771b9a312d03 (diff) | |
Merge branch 'avif'
Merges PR #399
Diffstat (limited to 'server/szurubooru')
| -rw-r--r-- | server/szurubooru/func/image_hash.py | 4 | ||||
| -rw-r--r-- | server/szurubooru/func/images.py | 16 | ||||
| -rw-r--r-- | server/szurubooru/func/mime.py | 22 | ||||
| -rw-r--r-- | server/szurubooru/func/posts.py | 3 | ||||
| -rw-r--r-- | server/szurubooru/tests/assets/avif-avis.avif | bin | 0 -> 2698 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/assets/avif-similar.avif | bin | 0 -> 2281 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/assets/avif.avif | bin | 0 -> 2698 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/assets/heic-heix.heic | bin | 0 -> 2806 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/assets/heic.heic | bin | 0 -> 2806 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/assets/heif-similar.heif | bin | 0 -> 3480 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/assets/heif.heif | bin | 0 -> 3489 bytes | |||
| -rw-r--r-- | server/szurubooru/tests/func/test_image_hash.py | 50 | ||||
| -rw-r--r-- | server/szurubooru/tests/func/test_mime.py | 37 | ||||
| -rw-r--r-- | server/szurubooru/tests/func/test_posts.py | 67 |
14 files changed, 197 insertions, 2 deletions
diff --git a/server/szurubooru/func/image_hash.py b/server/szurubooru/func/image_hash.py index fc7d141..8d1cd2a 100644 --- a/server/szurubooru/func/image_hash.py +++ b/server/szurubooru/func/image_hash.py @@ -6,6 +6,10 @@ from typing import Any, Callable, List, Optional, Set, Tuple import numpy as np from PIL import Image +import pillow_avif +import pyheif +from pyheif_pillow_opener import register_heif_opener +register_heif_opener() from szurubooru import config, errors diff --git a/server/szurubooru/func/images.py b/server/szurubooru/func/images.py index 6413ac8..101bba8 100644 --- a/server/szurubooru/func/images.py +++ b/server/szurubooru/func/images.py @@ -4,7 +4,9 @@ import math import re import shlex import subprocess +from io import BytesIO from typing import List +from PIL import Image as PILImage from szurubooru import errors from szurubooru.func import mime, util @@ -12,6 +14,13 @@ from szurubooru.func import mime, util logger = logging.getLogger(__name__) +def convert_heif_to_png(content: bytes) -> bytes: + img = PILImage.open(BytesIO(content)) + img_byte_arr = BytesIO() + img.save(img_byte_arr, format='PNG') + return img_byte_arr.getvalue() + + class Image: def __init__(self, content: bytes) -> None: self.content = content @@ -252,7 +261,12 @@ class Image: ignore_error_if_data: bool = False, get_logs: bool = False, ) -> bytes: - extension = mime.get_extension(mime.get_mime_type(self.content)) + mime_type = mime.get_mime_type(self.content) + if mime.is_heif(mime_type): + # FFmpeg does not support HEIF. + # https://trac.ffmpeg.org/ticket/6521 + self.content = convert_heif_to_png(self.content) + extension = mime.get_extension(mime_type) assert extension with util.create_temp_file(suffix="." + extension) as handle: handle.write(self.content) diff --git a/server/szurubooru/func/mime.py b/server/szurubooru/func/mime.py index e521c25..93c096b 100644 --- a/server/szurubooru/func/mime.py +++ b/server/szurubooru/func/mime.py @@ -24,6 +24,15 @@ def get_mime_type(content: bytes) -> str: if content[0:2] == b"BM": return "image/bmp" + if content[4:12] in (b"ftypavif", b"ftypavis"): + return "image/avif" + + if content[4:12] == b"ftypmif1": + return "image/heif" + + if content[4:12] in (b"ftypheic", b"ftypheix"): + return "image/heic" + if content[0:4] == b"\x1A\x45\xDF\xA3": return "video/webm" @@ -41,6 +50,9 @@ def get_extension(mime_type: str) -> Optional[str]: "image/png": "png", "image/webp": "webp", "image/bmp": "bmp", + "image/avif": "avif", + "image/heif": "heif", + "image/heic": "heic", "video/mp4": "mp4", "video/webm": "webm", "application/octet-stream": "dat", @@ -63,6 +75,9 @@ def is_image(mime_type: str) -> bool: "image/gif", "image/webp", "image/bmp", + "image/avif", + "image/heif", + "image/heic", ) @@ -72,3 +87,10 @@ def is_animated_gif(content: bytes) -> bool: get_mime_type(content) == "image/gif" and len(re.findall(pattern, content)) > 1 ) + +def is_heif(mime_type: str) -> bool: + return mime_type.lower() in ( + "image/heif", + "image/heic", + "image/avif", + ) diff --git a/server/szurubooru/func/posts.py b/server/szurubooru/func/posts.py index 0493681..be2259c 100644 --- a/server/szurubooru/func/posts.py +++ b/server/szurubooru/func/posts.py @@ -650,7 +650,8 @@ def update_post_content(post: model.Post, content: Optional[bytes]) -> None: image = images.Image(content) post.canvas_width = image.width post.canvas_height = image.height - except errors.ProcessingError: + except errors.ProcessingError as ex: + logger.exception(ex) if not config.config["allow_broken_uploads"]: raise InvalidPostContentError("Unable to process image metadata") else: diff --git a/server/szurubooru/tests/assets/avif-avis.avif b/server/szurubooru/tests/assets/avif-avis.avif Binary files differnew file mode 100644 index 0000000..3b27f29 --- /dev/null +++ b/server/szurubooru/tests/assets/avif-avis.avif diff --git a/server/szurubooru/tests/assets/avif-similar.avif b/server/szurubooru/tests/assets/avif-similar.avif Binary files differnew file mode 100644 index 0000000..476f3f5 --- /dev/null +++ b/server/szurubooru/tests/assets/avif-similar.avif diff --git a/server/szurubooru/tests/assets/avif.avif b/server/szurubooru/tests/assets/avif.avif Binary files differnew file mode 100644 index 0000000..edf4960 --- /dev/null +++ b/server/szurubooru/tests/assets/avif.avif diff --git a/server/szurubooru/tests/assets/heic-heix.heic b/server/szurubooru/tests/assets/heic-heix.heic Binary files differnew file mode 100644 index 0000000..27d1268 --- /dev/null +++ b/server/szurubooru/tests/assets/heic-heix.heic diff --git a/server/szurubooru/tests/assets/heic.heic b/server/szurubooru/tests/assets/heic.heic Binary files differnew file mode 100644 index 0000000..f1d2ed0 --- /dev/null +++ b/server/szurubooru/tests/assets/heic.heic diff --git a/server/szurubooru/tests/assets/heif-similar.heif b/server/szurubooru/tests/assets/heif-similar.heif Binary files differnew file mode 100644 index 0000000..54385f2 --- /dev/null +++ b/server/szurubooru/tests/assets/heif-similar.heif diff --git a/server/szurubooru/tests/assets/heif.heif b/server/szurubooru/tests/assets/heif.heif Binary files differnew file mode 100644 index 0000000..7fec72b --- /dev/null +++ b/server/szurubooru/tests/assets/heif.heif diff --git a/server/szurubooru/tests/func/test_image_hash.py b/server/szurubooru/tests/func/test_image_hash.py index e7028b6..5a5dc71 100644 --- a/server/szurubooru/tests/func/test_image_hash.py +++ b/server/szurubooru/tests/func/test_image_hash.py @@ -27,3 +27,53 @@ def test_signature_functions(read_asset, config_injector): words2 = image_hash.generate_words(sig2) words_match = sum(word1 == word2 for word1, word2 in zip(words1, words2)) assert words_match == 18 + + +def test_signature_heif(read_asset, config_injector): + sig1 = image_hash.generate_signature(read_asset("heif.heif")) + sig2 = image_hash.generate_signature(read_asset("heif-similar.heif")) + + sig1_repacked = image_hash.unpack_signature( + image_hash.pack_signature(sig1) + ) + sig2_repacked = image_hash.unpack_signature( + image_hash.pack_signature(sig2) + ) + assert array_equal(sig1, sig1_repacked) + assert array_equal(sig2, sig2_repacked) + + dist1 = image_hash.normalized_distance([sig1], sig2) + assert abs(dist1[0] - 0.136777724290135) < 1e-8 + + dist2 = image_hash.normalized_distance([sig2], sig2) + assert abs(dist2[0]) < 1e-8 + + words1 = image_hash.generate_words(sig1) + words2 = image_hash.generate_words(sig2) + words_match = sum(word1 == word2 for word1, word2 in zip(words1, words2)) + assert words_match == 43 + + +def test_signature_avif(read_asset, config_injector): + sig1 = image_hash.generate_signature(read_asset("avif.avif")) + sig2 = image_hash.generate_signature(read_asset("avif-similar.avif")) + + sig1_repacked = image_hash.unpack_signature( + image_hash.pack_signature(sig1) + ) + sig2_repacked = image_hash.unpack_signature( + image_hash.pack_signature(sig2) + ) + assert array_equal(sig1, sig1_repacked) + assert array_equal(sig2, sig2_repacked) + + dist1 = image_hash.normalized_distance([sig1], sig2) + assert abs(dist1[0] - 0.22628712858355998) < 1e-8 + + dist2 = image_hash.normalized_distance([sig2], sig2) + assert abs(dist2[0]) < 1e-8 + + words1 = image_hash.generate_words(sig1) + words2 = image_hash.generate_words(sig2) + words_match = sum(word1 == word2 for word1, word2 in zip(words1, words2)) + assert words_match == 12 diff --git a/server/szurubooru/tests/func/test_mime.py b/server/szurubooru/tests/func/test_mime.py index 62fd4a7..b33746b 100644 --- a/server/szurubooru/tests/func/test_mime.py +++ b/server/szurubooru/tests/func/test_mime.py @@ -14,6 +14,11 @@ from szurubooru.func import mime ("gif.gif", "image/gif"), ("webp.webp", "image/webp"), ("bmp.bmp", "image/bmp"), + ("avif.avif", "image/avif"), + ("avif-avis.avif", "image/avif"), + ("heif.heif", "image/heif"), + ("heic.heic", "image/heic"), + ("heic-heix.heic", "image/heic"), ("text.txt", "application/octet-stream"), ], ) @@ -36,6 +41,9 @@ def test_get_mime_type_for_empty_file(): ("image/gif", "gif"), ("image/webp", "webp"), ("image/bmp", "bmp"), + ("image/avif", "avif"), + ("image/heif", "heif"), + ("image/heic", "heic"), ("application/octet-stream", "dat"), ], ) @@ -78,10 +86,16 @@ def test_is_video(input_mime_type, expected_state): ("image/png", True), ("image/jpeg", True), ("image/bmp", True), + ("image/avif", True), + ("image/heic", True), + ("image/heif", True), ("IMAGE/GIF", True), ("IMAGE/PNG", True), ("IMAGE/JPEG", True), ("IMAGE/BMP", True), + ("IMAGE/AVIF", True), + ("IMAGE/HEIC", True), + ("IMAGE/HEIF", True), ("image/anything_else", False), ("not an image", False), ], @@ -99,3 +113,26 @@ def test_is_image(input_mime_type, expected_state): ) def test_is_animated_gif(read_asset, input_path, expected_state): assert mime.is_animated_gif(read_asset(input_path)) == expected_state + + +@pytest.mark.parametrize( + "input_mime_type,expected_state", + [ + ("image/gif", False), + ("image/png", False), + ("image/jpeg", False), + ("image/avif", True), + ("image/heic", True), + ("image/heif", True), + ("IMAGE/GIF", False), + ("IMAGE/PNG", False), + ("IMAGE/JPEG", False), + ("IMAGE/AVIF", True), + ("IMAGE/HEIC", True), + ("IMAGE/HEIF", True), + ("image/anything_else", False), + ("not an image", False), + ], +) +def test_is_heif(input_mime_type, expected_state): + assert mime.is_heif(input_mime_type) == expected_state diff --git a/server/szurubooru/tests/func/test_posts.py b/server/szurubooru/tests/func/test_posts.py index 0693741..fa1b3bb 100644 --- a/server/szurubooru/tests/func/test_posts.py +++ b/server/szurubooru/tests/func/test_posts.py @@ -401,6 +401,41 @@ def test_update_post_source_with_too_long_string(): ), ( False, + "avif.avif", + "image/avif", + model.Post.TYPE_IMAGE, + "1_244c8840887984c4.avif", + ), + ( + False, + "avif-avis.avif", + "image/avif", + model.Post.TYPE_IMAGE, + "1_244c8840887984c4.avif", + ), + ( + False, + "heic.heic", + "image/heic", + model.Post.TYPE_IMAGE, + "1_244c8840887984c4.heic", + ), + ( + False, + "heic-heix.heic", + "image/heic", + model.Post.TYPE_IMAGE, + "1_244c8840887984c4.heic", + ), + ( + False, + "heif.heif", + "image/heif", + model.Post.TYPE_IMAGE, + "1_244c8840887984c4.heif", + ), + ( + False, "gif-animated.gif", "image/gif", model.Post.TYPE_ANIMATION, @@ -706,6 +741,38 @@ def test_update_post_content_leaving_custom_thumbnail( assert os.path.exists(generated_path) +@pytest.mark.parametrize("filename", ("avif.avif", "heic.heic", "heif.heif")) +def test_update_post_content_convert_heif_to_png_when_processing( + tmpdir, config_injector, read_asset, post_factory, filename +): + config_injector( + { + "data_dir": str(tmpdir.mkdir("data")), + "thumbnails": { + "post_width": 300, + "post_height": 300, + }, + "secret": "test", + "allow_broken_uploads": False, + } + ) + post = post_factory(id=1) + db.session.add(post) + posts.update_post_content(post, read_asset(filename)) + posts.update_post_thumbnail(post, read_asset(filename)) + db.session.flush() + generated_path = ( + "{}/data/generated-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.jpg" + ) + source_path = ( + "{}/data/posts/custom-thumbnails/".format(tmpdir) + + "1_244c8840887984c4.dat" + ) + assert os.path.exists(source_path) + assert os.path.exists(generated_path) + + def test_update_post_tags(tag_factory): post = model.Post() with patch("szurubooru.func.tags.get_or_create_tags_by_names"): |