test_image_info

tests/imagetools/test_image_info.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
from __future__ import annotations

import dataclasses
import io
from pathlib import Path
from xml.sax import saxutils

import PIL
import pytest

from lektor_ng.imagetools.image_info import (
    _parse_svg_units_px,
    _save_position,
    get_image_info,
)

EXIF_ORIENTATION_TAG = 0x0112


@pytest.mark.parametrize(
    "dimension, pixels",
    [
        ("123", 123),
        ("50%", None),
        ("42px", 42),
        ("42.5 px", 42.5),
    ],
)
def test_parse_svg_units_px(dimension, pixels):
    assert _parse_svg_units_px(dimension) == pixels


class DummyFileBase:
    def write(self, fp):
        raise NotImplementedError()

    def open(self):
        fp = io.BytesIO()
        self.write(fp)
        fp.seek(0)
        return fp

    def path(self, tmp_path: Path) -> Path:
        path = tmp_path / "dummy"
        with path.open("wb") as fp:
            self.write(fp)
        return path


@dataclasses.dataclass
class DummyImage(DummyFileBase):
    width: int
    height: int
    format: str = "PNG"
    orientation: int | None = None

    def write(self, fp):
        image = PIL.Image.new("RGB", (self.width, self.height), "#999")
        exif = image.getexif()
        if self.orientation is not None:
            exif[EXIF_ORIENTATION_TAG] = self.orientation
        image.save(fp, self.format, exif=exif)


@dataclasses.dataclass
class DummySVGImage(DummyFileBase):
    width: int | str | None = None
    height: int | str | None = None
    xmlns: str | None = "http://www.w3.org/2000/svg"
    xml_decl: str | None = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n'

    def write(self, fp):
        values = dataclasses.asdict(self)
        svg_attrs = "".join(
            f" {key}={saxutils.quoteattr(str(values[key]))}"
            for key in ("width", "height", "xmlns")
            if values[key] is not None
        )
        svg = (
            f"{self.xml_decl or ''}"
            f"<svg{svg_attrs}>"
            '<circle cx="50" cy="50" r="50" fill="lightgrey"/>'
            '<circle cx="30" cy="30" r="10" fill="black"/>'
            '<circle cx="60" cy="30" r="10" fill="black"/>'
            '<path stroke="black" d="M30 70 l30 0" stroke-width="5"/>'
            "</svg>"
        )
        fp.write(svg.encode("utf-8"))


@dataclasses.dataclass
class DummyFile(DummyFileBase):
    content: bytes = b""

    def write(self, fp):
        fp.write(self.content)


@pytest.mark.parametrize(
    "image, expected",
    [
        (DummyFile(b"short"), (None, None, None)),
        (DummyFile(b"Not an image. " * 40), (None, None, None)),
        (DummyImage(10, 20, "JPEG"), ("jpeg", 10, 20)),
        (DummyImage(10, 20, "GIF"), ("gif", 10, 20)),
        (DummyImage(10, 20, "PNG"), ("png", 10, 20)),
        (DummyImage(10, 20, "JPEG", orientation=5), ("jpeg", 20, 10)),
        # check that Exif orientation is ignored for PNG images"
        (DummyImage(10, 20, "PNG", orientation=7), ("png", 10, 20)),
        (DummyImage(10, 20, "PPM"), (None, None, None)),
        (DummySVGImage("10px", "20px"), ("svg", 10, 20)),
        (DummySVGImage("10", "20", xml_decl=None), ("svg", 10, 20)),
        (DummySVGImage(None, None), ("svg", None, None)),
        (DummySVGImage("invalid-width", "invalid-height"), ("svg", None, None)),
        (DummySVGImage("10px", "10px", xmlns=None), (None, None, None)),
    ],
)
def test_get_image_info(image, expected, tmp_path):
    image_path = tmp_path / "test-image"
    with image_path.open("wb") as fp:
        image.write(fp)
    assert get_image_info(image_path) == expected


def test_get_image_info_fp_deprecated():
    image = DummyImage(10, 20, "JPEG")
    with pytest.deprecated_call() as w:
        assert get_image_info(image.open()) == ("jpeg", 10, 20)
    for warning in w:
        assert warning.filename == __file__


def test_save_position():
    fp = io.BytesIO(b"data")
    with _save_position(fp) as infp:
        assert infp.read() == b"data"
    assert fp.read() == b"data"


@pytest.fixture(
    params=[
        "test.jpg",  # base image, exif-rotated
        "test-sof-last.jpg",  # same image but with SOF marker last
        "test-progressive.jpg",  # with progressive encoding, rotated in place
    ]
)
def demo_test_jpg(request, pad):
    return pad.root.attachments.images.get(request.param)


def test_image_attributes(demo_test_jpg):
    assert demo_test_jpg.width == 384
    assert demo_test_jpg.height == 512
    assert demo_test_jpg.format == "jpeg"