1"""Tests for `chives.media`."""
3from pathlib import Path
9from chives.media import (
18def fixtures_dir() -> Path:
20 Return the directory where media fixtures are stored.
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:
36 Tests for create_image_entity().
39 def test_basic_image(self, fixtures_dir: Path) -> None:
41 Get an image entity for a basic blue square.
43 entity = create_image_entity(fixtures_dir / "blue.png")
46 "path": "tests/fixtures/media/blue.png",
49 "tint_colour": "#0000ff",
52 @pytest.mark.parametrize(
55 # This is a solid blue image with a section in the middle deleted
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
64 def test_image_with_transparency(self, fixtures_dir: Path, filename: str) -> None:
66 If an image has transparent pixels, then the entity has
67 `has_transparency=True`.
69 entity = create_image_entity(fixtures_dir / filename)
70 assert entity["has_transparency"]
72 @pytest.mark.parametrize(
78 # An animated electric field drawn in TikZ.
79 # Downloaded from https://tex.stackexchange.com/a/158930/9668
83 def test_image_without_transparency(
84 self, fixtures_dir: Path, filename: str
87 If an image has no transparent pixels, then the entity doesn't
88 have a `has_transparency` key.
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(
110 def test_accounts_for_exif_orientation(
111 self, fixtures_dir: Path, filename: str
114 The dimensions are the display dimensions, which accounts for
115 the EXIF orientation.
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:
122 If an image is animated, the entity has `is_animated=True`.
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:
131 The `alt_text` and `source_url` values are forwarded to the
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",
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
147 You can't pass `alt_text` and `generate_transcript` at the same time.
149 with pytest.raises(TypeError):
151 fixtures_dir / "blue.png",
152 alt_text="This is the alt text",
153 generate_transcript=True,
156 def test_generate_transcript(self, fixtures_dir: Path) -> None:
158 If you pass `generate_transcript=True`, the image is OCR'd for alt text.
160 entity = create_image_entity(
161 fixtures_dir / "underlined_text.png", generate_transcript=True
163 assert entity["alt_text"] == "I visited Berlin in Germany."
165 def test_generate_transcript_if_no_text(self, fixtures_dir: Path) -> None:
167 If you pass `generate_transcript=True` for an image with no text,
168 you don't get any alt text.
170 entity = create_image_entity(
171 fixtures_dir / "blue.png", generate_transcript=True
173 assert "alt_text" not in entity
175 def test_create_thumbnail_by_width(
176 self, fixtures_dir: Path, tmp_path: Path
179 Create a thumbnail by width.
181 entity = create_image_entity(
182 fixtures_dir / "blue.png",
183 thumbnail_config={"out_dir": tmp_path / "thumbnails", "width": 10},
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
195 Create a thumbnail by height.
197 entity = create_image_entity(
198 fixtures_dir / "blue.png",
199 thumbnail_config={"out_dir": tmp_path / "thumbnails", "height": 5},
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",
210 ("white", "#005493"),
211 ("black", "#b3fdff"),
212 ("#111111", "#b3fdff"),
215 def test_tint_colour_is_based_on_background(
216 self, fixtures_dir: Path, background: str, tint_colour: str
219 The tint colour is based to suit the background.
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
226 assert entity["tint_colour"] == tint_colour
229class TestCreateVideoEntity:
231 Tests for `create_video_entity()`.
234 def test_basic_video(self, fixtures_dir: Path) -> None:
236 Get a video entity for a basic video.
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",
246 "path": "tests/fixtures/media/Sintel_360_10s_1MB_H264.mp4",
249 "duration": "0:00:10.000000",
252 "path": "tests/fixtures/media/Sintel_360_10s_1MB_H264.png",
253 "tint_colour": "#020202",
259 def test_other_attrs_are_forwarded(self, fixtures_dir: Path) -> None:
261 The `subtitles_path`, `source_url` and `autoplay` values are
262 forwarded to the final entity.
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",
272 assert entity["subtitles"] == [
274 "path": "tests/fixtures/media/Sintel_360_10s_1MB_H264.en.vtt",
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:
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/
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",
294 assert entity["width"] == 1350
295 assert entity["height"] == 1080
297 def test_video_without_sample_aspect_ratio(self, fixtures_dir: Path) -> None:
299 Get the width/height dimensions of a video that doesn't have
300 `sample_aspect_ratio` in its metadata.
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",
308 assert entity["width"] == 960
309 assert entity["height"] == 720
311 @pytest.mark.parametrize(
312 "background, tint_colour",
314 ("white", "#005493"),
315 ("black", "#b3fdff"),
316 ("#111111", "#b3fdff"),
319 def test_tint_colour_is_based_on_background(
320 self, fixtures_dir: Path, background: str, tint_colour: str
323 The tint colour is based to suit the background.
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,
332 assert entity["poster"]["tint_colour"] == tint_colour
334 def test_video_with_thumbnail(self, fixtures_dir: Path, tmp_path: Path) -> None:
336 Create a low-resolution thumbnail of the poster image.
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},
344 assert entity["poster"]["thumbnail_path"] == str(
345 tmp_path / "thumbnails/Sintel_360_10s_1MB_H264.png"
347 assert Path(entity["poster"]["thumbnail_path"]).exists()
350class TestGetMediaPaths:
352 Tests for `get_media_paths`.
355 def test_basic_image(self, fixtures_dir: Path) -> None:
357 An image with no thumbnail only has one path: the image.
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:
364 An image with a thumbnail has two paths: the video and the
367 entity = create_image_entity(
368 fixtures_dir / "blue.png",
369 thumbnail_config={"out_dir": tmp_path / "thumbnails", "width": 300},
371 assert get_media_paths(entity) == {
372 fixtures_dir / "blue.png",
373 tmp_path / "thumbnails/blue.png",
376 def test_video(self, fixtures_dir: Path) -> None:
378 A video has two paths: the video and the poster image.
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",
384 assert get_media_paths(entity) == {
385 fixtures_dir / "Sintel_360_10s_1MB_H264.mp4",
386 fixtures_dir / "Sintel_360_10s_1MB_H264.png",
389 def test_video_with_subtitles(self, fixtures_dir: Path) -> None:
391 A video with subtitles has three paths: the video, the subtitles,
392 and the poster image.
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",
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",
405 def test_video_with_thumbnail(self, fixtures_dir: Path, tmp_path: Path) -> None:
407 A video with a poster thumbnail has three paths: the video,
408 the poster image, and the poster thumbnail.
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},
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",
421 @pytest.mark.parametrize("bad_entity", [{}, {"type": "shape"}])
422 def test_unrecognised_entity_is_error(self, bad_entity: Any) -> None:
424 Getting media paths for an unrecognised entity type is a TypeError.
426 with pytest.raises(TypeError):
427 get_media_paths(bad_entity)