Skip to main content

tests/test_media.py

1"""Tests for `chives.media`."""
3from pathlib import Path
4from typing import Any
6from PIL import Image
7import pytest
9from chives.media import (
10 create_image_entity,
11 create_video_entity,
12 get_media_paths,
13 is_av1_video,
17@pytest.fixture
18def fixtures_dir() -> Path:
19 """
20 Return the directory where media fixtures are stored.
21 """
22 return Path("tests/fixtures/media")
25def test_is_av1_video(fixtures_dir: Path) -> None:
26 """is_av1_video correctly detects AV1 videos."""
27 # These two videos were downloaded from
28 # https://test-videos.co.uk/sintel/mp4-h264 and
29 # https://test-videos.co.uk/sintel/mp4-av1
30 assert not is_av1_video(fixtures_dir / "Sintel_360_10s_1MB_H264.mp4")
31 assert is_av1_video(fixtures_dir / "Sintel_360_10s_1MB_AV1.mp4")
34class TestCreateImageEntity:
35 """
36 Tests for create_image_entity().
37 """
39 def test_basic_image(self, fixtures_dir: Path) -> None:
40 """
41 Get an image entity for a basic blue square.
42 """
43 entity = create_image_entity(fixtures_dir / "blue.png")
44 assert entity == {
45 "type": "image",
46 "path": "tests/fixtures/media/blue.png",
47 "width": 32,
48 "height": 16,
49 "tint_colour": "#0000ff",
50 }
52 @pytest.mark.parametrize(
53 "filename",
54 [
55 # This is a solid blue image with a section in the middle deleted
56 "blue_with_hole.png",
57 #
58 # An asteroid belt drawn in TikZ by TeX.SE user Qrrbrbirlbel,
59 # which has `transparency` in its im.info.
60 # Downloaded from http://tex.stackexchange.com/a/111974/9668
61 "asteroid_belt.png",
62 ],
63 )
64 def test_image_with_transparency(self, fixtures_dir: Path, filename: str) -> None:
65 """
66 If an image has transparent pixels, then the entity has
67 `has_transparency=True`.
68 """
69 entity = create_image_entity(fixtures_dir / filename)
70 assert entity["has_transparency"]
72 @pytest.mark.parametrize(
73 "filename",
74 [
75 "blue.png",
76 "space.jpg",
77 #
78 # An animated electric field drawn in TikZ.
79 # Downloaded from https://tex.stackexchange.com/a/158930/9668
80 "electric_field.gif",
81 ],
82 )
83 def test_image_without_transparency(
84 self, fixtures_dir: Path, filename: str
85 ) -> None:
86 """
87 If an image has no transparent pixels, then the entity doesn't
88 have a `has_transparency` key.
89 """
90 entity = create_image_entity(fixtures_dir / filename)
91 assert "has_transparency" not in entity
93 # These test files were downloaded from Dave Perrett repo:
94 # https://github.com/recurser/exif-orientation-examples
96 @pytest.mark.parametrize(
97 "filename",
98 [
99 "Landscape_0.jpg",
100 "Landscape_1.jpg",
101 "Landscape_2.jpg",
102 "Landscape_3.jpg",
103 "Landscape_4.jpg",
104 "Landscape_5.jpg",
105 "Landscape_6.jpg",
106 "Landscape_7.jpg",
107 "Landscape_8.jpg",
108 ],
109 )
110 def test_accounts_for_exif_orientation(
111 self, fixtures_dir: Path, filename: str
112 ) -> None:
113 """
114 The dimensions are the display dimensions, which accounts for
115 the EXIF orientation.
116 """
117 entity = create_image_entity(fixtures_dir / filename)
118 assert (entity["width"], entity["height"]) == (1800, 1200)
120 def test_animated_image(self, fixtures_dir: Path) -> None:
121 """
122 If an image is animated, the entity has `is_animated=True`.
123 """
124 # An animated electric field drawn in TikZ.
125 # Downloaded from https://tex.stackexchange.com/a/158930/9668
126 entity = create_image_entity(fixtures_dir / "electric_field.gif")
127 assert entity["is_animated"]
129 def test_other_attrs_are_forwarded(self, fixtures_dir: Path) -> None:
130 """
131 The `alt_text` and `source_url` values are forwarded to the
132 final entity.
133 """
134 entity = create_image_entity(
135 fixtures_dir / "blue.png",
136 alt_text="This is the alt text",
137 source_url="https://example.com/blue.png",
138 )
140 assert entity["alt_text"] == "This is the alt text"
141 assert entity["source_url"] == "https://example.com/blue.png"
143 def test_alt_text_and_generate_transcript_is_error(
144 self, fixtures_dir: Path
145 ) -> None:
146 """
147 You can't pass `alt_text` and `generate_transcript` at the same time.
148 """
149 with pytest.raises(TypeError):
150 create_image_entity(
151 fixtures_dir / "blue.png",
152 alt_text="This is the alt text",
153 generate_transcript=True,
154 )
156 def test_generate_transcript(self, fixtures_dir: Path) -> None:
157 """
158 If you pass `generate_transcript=True`, the image is OCR'd for alt text.
159 """
160 entity = create_image_entity(
161 fixtures_dir / "underlined_text.png", generate_transcript=True
162 )
163 assert entity["alt_text"] == "I visited Berlin in Germany."
165 def test_generate_transcript_if_no_text(self, fixtures_dir: Path) -> None:
166 """
167 If you pass `generate_transcript=True` for an image with no text,
168 you don't get any alt text.
169 """
170 entity = create_image_entity(
171 fixtures_dir / "blue.png", generate_transcript=True
172 )
173 assert "alt_text" not in entity
175 def test_create_thumbnail_by_width(
176 self, fixtures_dir: Path, tmp_path: Path
177 ) -> None:
178 """
179 Create a thumbnail by width.
180 """
181 entity = create_image_entity(
182 fixtures_dir / "blue.png",
183 thumbnail_config={"out_dir": tmp_path / "thumbnails", "width": 10},
184 )
186 assert Path(entity["thumbnail_path"]).exists()
188 with Image.open(entity["thumbnail_path"]) as im:
189 assert im.width == 10
191 def test_create_thumbnail_by_height(
192 self, fixtures_dir: Path, tmp_path: Path
193 ) -> None:
194 """
195 Create a thumbnail by height.
196 """
197 entity = create_image_entity(
198 fixtures_dir / "blue.png",
199 thumbnail_config={"out_dir": tmp_path / "thumbnails", "height": 5},
200 )
202 assert Path(entity["thumbnail_path"]).exists()
204 with Image.open(entity["thumbnail_path"]) as im:
205 assert im.height == 5
207 @pytest.mark.parametrize(
208 "background, tint_colour",
209 [
210 ("white", "#005493"),
211 ("black", "#b3fdff"),
212 ("#111111", "#b3fdff"),
213 ],
214 )
215 def test_tint_colour_is_based_on_background(
216 self, fixtures_dir: Path, background: str, tint_colour: str
217 ) -> None:
218 """
219 The tint colour is based to suit the background.
220 """
221 # This is a checkerboard pattern made of 2 different shades of
222 # turquoise, a light and a dark.
223 entity = create_image_entity(
224 fixtures_dir / "checkerboard.png", background=background
225 )
226 assert entity["tint_colour"] == tint_colour
229class TestCreateVideoEntity:
230 """
231 Tests for `create_video_entity()`.
232 """
234 def test_basic_video(self, fixtures_dir: Path) -> None:
235 """
236 Get a video entity for a basic video.
237 """
238 # This video was downloaded from
239 # https://test-videos.co.uk/sintel/mp4-h264
240 entity = create_video_entity(
241 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
242 poster_path=fixtures_dir / "Sintel_360_10s_1MB_H264.png",
243 )
244 assert entity == {
245 "type": "video",
246 "path": "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
247 "width": 640,
248 "height": 360,
249 "duration": "0:00:10.000000",
250 "poster": {
251 "type": "image",
252 "path": "tests/fixtures/media/Sintel_360_10s_1MB_H264.png",
253 "tint_colour": "#020202",
254 "width": 640,
255 "height": 360,
256 },
257 }
259 def test_other_attrs_are_forwarded(self, fixtures_dir: Path) -> None:
260 """
261 The `subtitles_path`, `source_url` and `autoplay` values are
262 forwarded to the final entity.
263 """
264 entity = create_video_entity(
265 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
266 poster_path=fixtures_dir / "Sintel_360_10s_1MB_H264.png",
267 subtitles_path=fixtures_dir / "Sintel_360_10s_1MB_H264.en.vtt",
268 source_url="https://test-videos.co.uk/sintel/mp4-h264",
269 autoplay=True,
270 )
272 assert entity["subtitles"] == [
273 {
274 "path": "tests/fixtures/media/Sintel_360_10s_1MB_H264.en.vtt",
275 "label": "English",
276 }
277 ]
278 assert entity["source_url"] == "https://test-videos.co.uk/sintel/mp4-h264"
279 assert entity["autoplay"]
281 def test_gets_display_dimensions(self, fixtures_dir: Path) -> None:
282 """
283 The width/height dimensions are based on the display aspect ratio,
284 not the storage aspect ratio.
286 See https://alexwlchan.net/2025/square-pixels/
287 """
288 # This is a short clip of https://www.youtube.com/watch?v=HHhyznZ2u4E
289 entity = create_video_entity(
290 fixtures_dir / "Mars 2020 EDL Remastered [HHhyznZ2u4E].mp4",
291 poster_path=fixtures_dir / "Mars 2020 EDL Remastered [HHhyznZ2u4E].jpg",
292 )
294 assert entity["width"] == 1350
295 assert entity["height"] == 1080
297 def test_video_without_sample_aspect_ratio(self, fixtures_dir: Path) -> None:
298 """
299 Get the width/height dimensions of a video that doesn't have
300 `sample_aspect_ratio` in its metadata.
301 """
302 # This is a short clip from Wings (1927).
303 entity = create_video_entity(
304 fixtures_dir / "wings_tracking_shot.mp4",
305 poster_path=fixtures_dir / "wings_tracking_shot.jpg",
306 )
308 assert entity["width"] == 960
309 assert entity["height"] == 720
311 @pytest.mark.parametrize(
312 "background, tint_colour",
313 [
314 ("white", "#005493"),
315 ("black", "#b3fdff"),
316 ("#111111", "#b3fdff"),
317 ],
318 )
319 def test_tint_colour_is_based_on_background(
320 self, fixtures_dir: Path, background: str, tint_colour: str
321 ) -> None:
322 """
323 The tint colour is based to suit the background.
324 """
325 # The poster image is a checkerboard pattern made of 2 different
326 # shades of turquoise, a light and a dark.
327 entity = create_video_entity(
328 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
329 poster_path=fixtures_dir / "checkerboard.png",
330 background=background,
331 )
332 assert entity["poster"]["tint_colour"] == tint_colour
334 def test_video_with_thumbnail(self, fixtures_dir: Path, tmp_path: Path) -> None:
335 """
336 Create a low-resolution thumbnail of the poster image.
337 """
338 entity = create_video_entity(
339 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
340 poster_path=fixtures_dir / "Sintel_360_10s_1MB_H264.png",
341 thumbnail_config={"out_dir": tmp_path / "thumbnails", "width": 300},
342 )
344 assert entity["poster"]["thumbnail_path"] == str(
345 tmp_path / "thumbnails/Sintel_360_10s_1MB_H264.png"
346 )
347 assert Path(entity["poster"]["thumbnail_path"]).exists()
350class TestGetMediaPaths:
351 """
352 Tests for `get_media_paths`.
353 """
355 def test_basic_image(self, fixtures_dir: Path) -> None:
356 """
357 An image with no thumbnail only has one path: the image.
358 """
359 entity = create_image_entity(fixtures_dir / "blue.png")
360 assert get_media_paths(entity) == {fixtures_dir / "blue.png"}
362 def test_image_with_thumbnail(self, fixtures_dir: Path, tmp_path: Path) -> None:
363 """
364 An image with a thumbnail has two paths: the video and the
365 thumbnail.
366 """
367 entity = create_image_entity(
368 fixtures_dir / "blue.png",
369 thumbnail_config={"out_dir": tmp_path / "thumbnails", "width": 300},
370 )
371 assert get_media_paths(entity) == {
372 fixtures_dir / "blue.png",
373 tmp_path / "thumbnails/blue.png",
374 }
376 def test_video(self, fixtures_dir: Path) -> None:
377 """
378 A video has two paths: the video and the poster image.
379 """
380 entity = create_video_entity(
381 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
382 poster_path=fixtures_dir / "Sintel_360_10s_1MB_H264.png",
383 )
384 assert get_media_paths(entity) == {
385 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
386 fixtures_dir / "Sintel_360_10s_1MB_H264.png",
387 }
389 def test_video_with_subtitles(self, fixtures_dir: Path) -> None:
390 """
391 A video with subtitles has three paths: the video, the subtitles,
392 and the poster image.
393 """
394 entity = create_video_entity(
395 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
396 poster_path=fixtures_dir / "Sintel_360_10s_1MB_H264.png",
397 subtitles_path=fixtures_dir / "Sintel_360_10s_1MB_H264.en.vtt",
398 )
399 assert get_media_paths(entity) == {
400 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
401 fixtures_dir / "Sintel_360_10s_1MB_H264.png",
402 fixtures_dir / "Sintel_360_10s_1MB_H264.en.vtt",
403 }
405 def test_video_with_thumbnail(self, fixtures_dir: Path, tmp_path: Path) -> None:
406 """
407 A video with a poster thumbnail has three paths: the video,
408 the poster image, and the poster thumbnail.
409 """
410 entity = create_video_entity(
411 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
412 poster_path=fixtures_dir / "Sintel_360_10s_1MB_H264.png",
413 thumbnail_config={"out_dir": tmp_path / "thumbnails", "width": 300},
414 )
415 assert get_media_paths(entity) == {
416 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
417 fixtures_dir / "Sintel_360_10s_1MB_H264.png",
418 tmp_path / "thumbnails/Sintel_360_10s_1MB_H264.png",
419 }
421 @pytest.mark.parametrize("bad_entity", [{}, {"type": "shape"}])
422 def test_unrecognised_entity_is_error(self, bad_entity: Any) -> None:
423 """
424 Getting media paths for an unrecognised entity type is a TypeError.
425 """
426 with pytest.raises(TypeError):
427 get_media_paths(bad_entity)