summaryrefslogtreecommitdiff
path: root/server/szurubooru
diff options
context:
space:
mode:
authorneobooru <50623835+neobooru@users.noreply.github.com>2021-06-01 16:57:29 +0200
committerneobooru <50623835+neobooru@users.noreply.github.com>2021-06-01 16:57:29 +0200
commit5ea9e27e483a01a5814d682134e753aedeaec305 (patch)
treef0e04646e967f0209bdbf008966dc941f00de3be /server/szurubooru
parent027e83a7e74d227f9552a52ea7a4b8ee6de989f4 (diff)
parent2949431d9a1b662dcbec68beb178771b9a312d03 (diff)
Merge branch 'avif'
Merges PR #399
Diffstat (limited to 'server/szurubooru')
-rw-r--r--server/szurubooru/func/image_hash.py4
-rw-r--r--server/szurubooru/func/images.py16
-rw-r--r--server/szurubooru/func/mime.py22
-rw-r--r--server/szurubooru/func/posts.py3
-rw-r--r--server/szurubooru/tests/assets/avif-avis.avifbin0 -> 2698 bytes
-rw-r--r--server/szurubooru/tests/assets/avif-similar.avifbin0 -> 2281 bytes
-rw-r--r--server/szurubooru/tests/assets/avif.avifbin0 -> 2698 bytes
-rw-r--r--server/szurubooru/tests/assets/heic-heix.heicbin0 -> 2806 bytes
-rw-r--r--server/szurubooru/tests/assets/heic.heicbin0 -> 2806 bytes
-rw-r--r--server/szurubooru/tests/assets/heif-similar.heifbin0 -> 3480 bytes
-rw-r--r--server/szurubooru/tests/assets/heif.heifbin0 -> 3489 bytes
-rw-r--r--server/szurubooru/tests/func/test_image_hash.py50
-rw-r--r--server/szurubooru/tests/func/test_mime.py37
-rw-r--r--server/szurubooru/tests/func/test_posts.py67
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
new file mode 100644
index 0000000..3b27f29
--- /dev/null
+++ b/server/szurubooru/tests/assets/avif-avis.avif
Binary files differ
diff --git a/server/szurubooru/tests/assets/avif-similar.avif b/server/szurubooru/tests/assets/avif-similar.avif
new file mode 100644
index 0000000..476f3f5
--- /dev/null
+++ b/server/szurubooru/tests/assets/avif-similar.avif
Binary files differ
diff --git a/server/szurubooru/tests/assets/avif.avif b/server/szurubooru/tests/assets/avif.avif
new file mode 100644
index 0000000..edf4960
--- /dev/null
+++ b/server/szurubooru/tests/assets/avif.avif
Binary files differ
diff --git a/server/szurubooru/tests/assets/heic-heix.heic b/server/szurubooru/tests/assets/heic-heix.heic
new file mode 100644
index 0000000..27d1268
--- /dev/null
+++ b/server/szurubooru/tests/assets/heic-heix.heic
Binary files differ
diff --git a/server/szurubooru/tests/assets/heic.heic b/server/szurubooru/tests/assets/heic.heic
new file mode 100644
index 0000000..f1d2ed0
--- /dev/null
+++ b/server/szurubooru/tests/assets/heic.heic
Binary files differ
diff --git a/server/szurubooru/tests/assets/heif-similar.heif b/server/szurubooru/tests/assets/heif-similar.heif
new file mode 100644
index 0000000..54385f2
--- /dev/null
+++ b/server/szurubooru/tests/assets/heif-similar.heif
Binary files differ
diff --git a/server/szurubooru/tests/assets/heif.heif b/server/szurubooru/tests/assets/heif.heif
new file mode 100644
index 0000000..7fec72b
--- /dev/null
+++ b/server/szurubooru/tests/assets/heif.heif
Binary files differ
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"):