mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-30 01:52:18 +00:00
refactor(images): read parsers top down and price azure edits in one function
This commit is contained in:
parent
4b01641190
commit
afff814038
3 changed files with 119 additions and 156 deletions
|
|
@ -9,17 +9,7 @@ from litellm._logging import verbose_logger
|
|||
from litellm.litellm_core_utils.token_counter import get_image_type
|
||||
from litellm.types.llms.openai import FileTypes
|
||||
|
||||
_JPEG_SOF_MARKERS: Final = frozenset(range(0xC0, 0xD0)) - {0xC4, 0xC8, 0xCC}
|
||||
_MAX_JPEG_SEGMENTS: Final = 1024
|
||||
_MAX_JPEG_HEADER_OFFSET: Final = 16 * 1024 * 1024
|
||||
_JPEG_FIRST_SEGMENT_OFFSET: Final = 2
|
||||
_JPEG_SOF_PAYLOAD_SIZE: Final = 5
|
||||
_JPEG_FILL_CHUNK: Final = 64 * 1024
|
||||
_HEADER_READ_SIZE: Final = 32
|
||||
_MIN_PNG_HEADER_SIZE: Final = 24
|
||||
_MIN_GIF_HEADER_SIZE: Final = 10
|
||||
_MIN_BMP_HEADER_SIZE: Final = 26
|
||||
_MIN_WEBP_HEADER_SIZE: Final = 30
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
|
|
@ -32,24 +22,65 @@ class ImageDimensions:
|
|||
return self.width * self.height
|
||||
|
||||
|
||||
def _content_stream(image: FileTypes) -> IO[bytes] | None:
|
||||
content: Final = image[1] if isinstance(image, tuple) else image
|
||||
if isinstance(content, bytes):
|
||||
return BytesIO(content)
|
||||
if isinstance(content, (str, os.PathLike)):
|
||||
def total_reference_pixels(images: Sequence[FileTypes]) -> int | None:
|
||||
measured: Final = tuple(read_image_dimensions(image) for image in images)
|
||||
dimensions: Final = tuple(size for size in measured if size is not None)
|
||||
if len(dimensions) != len(measured):
|
||||
verbose_logger.debug(
|
||||
"Reference image %d could not be measured (path, non-seekable stream, or no readable PNG, JPEG, WebP, "
|
||||
"GIF or BMP header); billing generated pixels only",
|
||||
measured.index(None),
|
||||
)
|
||||
return None
|
||||
return content if content.seekable() else None
|
||||
return sum(size.pixels for size in dimensions)
|
||||
|
||||
|
||||
def read_image_dimensions(image: FileTypes) -> ImageDimensions | None:
|
||||
try:
|
||||
content = image[1] if isinstance(image, tuple) else image
|
||||
if isinstance(content, (str, os.PathLike)):
|
||||
return None
|
||||
stream = BytesIO(content) if isinstance(content, bytes) else content
|
||||
if not stream.seekable():
|
||||
return None
|
||||
position = stream.tell()
|
||||
try:
|
||||
dimensions = _header_dimensions(stream, position)
|
||||
finally:
|
||||
stream.seek(position)
|
||||
if dimensions is None or dimensions.width <= 0 or dimensions.height <= 0:
|
||||
return None
|
||||
return dimensions
|
||||
except (OSError, ValueError, AttributeError, struct.error):
|
||||
return None
|
||||
|
||||
|
||||
def _header_dimensions(stream: IO[bytes], position: int) -> ImageDimensions | None:
|
||||
stream.seek(position)
|
||||
head: Final = stream.read(_HEADER_READ_SIZE)
|
||||
match get_image_type(head): # pyright: ignore[reportMatchNotExhaustive] # unmatched types fall through to the None below
|
||||
case "png":
|
||||
return _png_dimensions(head)
|
||||
case "webp":
|
||||
return _webp_dimensions(head)
|
||||
case "jpeg":
|
||||
return _jpeg_sof_dimensions(stream, position)
|
||||
case "gif":
|
||||
return _gif_dimensions(head)
|
||||
if head[:2] == b"BM":
|
||||
return _bmp_dimensions(head)
|
||||
return None
|
||||
|
||||
|
||||
def _png_dimensions(head: bytes) -> ImageDimensions | None:
|
||||
if len(head) < _MIN_PNG_HEADER_SIZE:
|
||||
if len(head) < 24:
|
||||
return None
|
||||
width, height = cast(tuple[int, int], struct.unpack(">II", head[16:24]))
|
||||
return ImageDimensions(width=width, height=height)
|
||||
|
||||
|
||||
def _gif_dimensions(head: bytes) -> ImageDimensions | None:
|
||||
if len(head) < _MIN_GIF_HEADER_SIZE:
|
||||
if len(head) < 10:
|
||||
return None
|
||||
width: Final = int.from_bytes(head[6:8], "little")
|
||||
height: Final = int.from_bytes(head[8:10], "little")
|
||||
|
|
@ -57,7 +88,7 @@ def _gif_dimensions(head: bytes) -> ImageDimensions | None:
|
|||
|
||||
|
||||
def _bmp_dimensions(head: bytes) -> ImageDimensions | None:
|
||||
if len(head) < _MIN_BMP_HEADER_SIZE:
|
||||
if len(head) < 26:
|
||||
return None
|
||||
if int.from_bytes(head[14:18], "little") == 12:
|
||||
core_width: Final = int.from_bytes(head[18:20], "little")
|
||||
|
|
@ -69,7 +100,7 @@ def _bmp_dimensions(head: bytes) -> ImageDimensions | None:
|
|||
|
||||
|
||||
def _webp_dimensions(head: bytes) -> ImageDimensions | None:
|
||||
if len(head) < _MIN_WEBP_HEADER_SIZE:
|
||||
if len(head) < 30:
|
||||
return None
|
||||
match head[12:16]: # pyright: ignore[reportMatchNotExhaustive] # unknown fourccs fall through to the None below
|
||||
case b"VP8X":
|
||||
|
|
@ -86,20 +117,32 @@ def _webp_dimensions(head: bytes) -> ImageDimensions | None:
|
|||
return None
|
||||
|
||||
|
||||
def _jpeg_sof_dimensions(stream: IO[bytes], start: int, offset: int, segments_left: int) -> ImageDimensions | None:
|
||||
segment_offset = offset # rebind-ok: advanced one JPEG segment per hop
|
||||
for _ in range(segments_left):
|
||||
if segment_offset > _MAX_JPEG_HEADER_OFFSET:
|
||||
_JPEG_SOF_MARKERS: Final = frozenset(range(0xC0, 0xD0)) - {0xC4, 0xC8, 0xCC}
|
||||
_JPEG_MAX_SEGMENTS: Final = 1024
|
||||
_JPEG_MAX_HEADER_OFFSET: Final = 16 * 1024 * 1024
|
||||
_JPEG_FIRST_SEGMENT_OFFSET: Final = 2
|
||||
_JPEG_SOF_PAYLOAD_SIZE: Final = 5
|
||||
_JPEG_FILL_CHUNK: Final = 64 * 1024
|
||||
|
||||
|
||||
def _jpeg_fill_run(stream: IO[bytes], start: int, offset: int) -> int:
|
||||
stream.seek(start + offset)
|
||||
fill: Final = stream.read(_JPEG_FILL_CHUNK)
|
||||
run: Final = len(fill) - len(fill.lstrip(b"\xff"))
|
||||
return run - 1
|
||||
|
||||
|
||||
def _jpeg_sof_dimensions(stream: IO[bytes], start: int) -> ImageDimensions | None:
|
||||
offset = _JPEG_FIRST_SEGMENT_OFFSET # rebind-ok: advanced one JPEG segment per hop
|
||||
for _ in range(_JPEG_MAX_SEGMENTS):
|
||||
if offset > _JPEG_MAX_HEADER_OFFSET:
|
||||
return None
|
||||
stream.seek(start + segment_offset)
|
||||
stream.seek(start + offset)
|
||||
marker = stream.read(4)
|
||||
if len(marker) < 4 or marker[0] != 0xFF:
|
||||
return None
|
||||
if marker[1] == 0xFF:
|
||||
stream.seek(start + segment_offset)
|
||||
fill = stream.read(_JPEG_FILL_CHUNK)
|
||||
run = len(fill) - len(fill.lstrip(b"\xff"))
|
||||
segment_offset += run - 1
|
||||
offset += _jpeg_fill_run(stream, start, offset)
|
||||
continue
|
||||
if marker[1] in _JPEG_SOF_MARKERS:
|
||||
sof = stream.read(_JPEG_SOF_PAYLOAD_SIZE)
|
||||
|
|
@ -110,56 +153,5 @@ def _jpeg_sof_dimensions(stream: IO[bytes], start: int, offset: int, segments_le
|
|||
segment_length = int.from_bytes(marker[2:4], "big")
|
||||
if segment_length < 2:
|
||||
return None
|
||||
segment_offset += 2 + segment_length
|
||||
offset += 2 + segment_length
|
||||
return None
|
||||
|
||||
|
||||
def _header_dimensions(stream: IO[bytes], position: int) -> ImageDimensions | None:
|
||||
stream.seek(position)
|
||||
head: Final = stream.read(_HEADER_READ_SIZE)
|
||||
match get_image_type(head): # pyright: ignore[reportMatchNotExhaustive] # unmatched types fall through to the None below
|
||||
case "png":
|
||||
return _png_dimensions(head)
|
||||
case "webp":
|
||||
return _webp_dimensions(head)
|
||||
case "jpeg":
|
||||
return _jpeg_sof_dimensions(stream, position, _JPEG_FIRST_SEGMENT_OFFSET, _MAX_JPEG_SEGMENTS)
|
||||
case "gif":
|
||||
return _gif_dimensions(head)
|
||||
if head[:2] == b"BM":
|
||||
return _bmp_dimensions(head)
|
||||
return None
|
||||
|
||||
|
||||
def _measure_stream(stream: IO[bytes]) -> ImageDimensions | None:
|
||||
position: Final = stream.tell()
|
||||
try:
|
||||
dimensions: Final = _header_dimensions(stream, position)
|
||||
finally:
|
||||
stream.seek(position)
|
||||
if dimensions is None or dimensions.width <= 0 or dimensions.height <= 0:
|
||||
return None
|
||||
return dimensions
|
||||
|
||||
|
||||
def read_image_dimensions(image: FileTypes) -> ImageDimensions | None:
|
||||
try:
|
||||
stream: Final = _content_stream(image)
|
||||
if stream is None:
|
||||
return None
|
||||
return _measure_stream(stream)
|
||||
except (OSError, ValueError, AttributeError, struct.error):
|
||||
return None
|
||||
|
||||
|
||||
def total_reference_pixels(images: Sequence[FileTypes]) -> int | None:
|
||||
measured: Final = tuple(read_image_dimensions(image) for image in images)
|
||||
dimensions: Final = tuple(size for size in measured if size is not None)
|
||||
if len(dimensions) != len(measured):
|
||||
verbose_logger.debug(
|
||||
"Reference image %d could not be measured (path, non-seekable stream, or no readable PNG, JPEG, WebP, "
|
||||
"GIF or BMP header); billing generated pixels only",
|
||||
measured.index(None),
|
||||
)
|
||||
return None
|
||||
return sum(size.pixels for size in dimensions)
|
||||
|
|
|
|||
|
|
@ -10,32 +10,20 @@ from litellm.litellm_core_utils.llm_cost_calc.utils import (
|
|||
)
|
||||
from litellm.types.utils import ImageResponse, ModelInfo
|
||||
|
||||
_SIZE_PATTERN: Final = re.compile(r"(\d+)(?:x|-x-)(\d+)")
|
||||
|
||||
|
||||
def _rate(table: ModelInfo, key: str) -> float | None:
|
||||
return _get_cost_per_unit(table, key, default_value=None)
|
||||
|
||||
|
||||
_SIZE_PATTERN: Final = re.compile(r"(\d+)(?:x|-x-)(\d+)")
|
||||
|
||||
|
||||
def _size_pixels(size: str | None) -> int:
|
||||
if size is None:
|
||||
return 0
|
||||
match: Final = _SIZE_PATTERN.fullmatch(size)
|
||||
if match is None or int(match[1]) <= 0 or int(match[2]) <= 0:
|
||||
return 0
|
||||
return int(match[1]) * int(match[2])
|
||||
|
||||
|
||||
def _generated_pixels(
|
||||
optional_params: Mapping[str, object] | None, size: str | None, image_response: ImageResponse
|
||||
) -> int:
|
||||
def _generated_pixels(optional_params: Mapping[str, object] | None, size: str | None) -> int:
|
||||
width: Final = optional_params.get("width") if optional_params else None
|
||||
height: Final = optional_params.get("height") if optional_params else None
|
||||
if type(width) is int and type(height) is int and width > 0 and height > 0:
|
||||
if isinstance(width, int) and isinstance(height, int):
|
||||
return width * height
|
||||
raw_size: Final = size or image_response.size
|
||||
return _size_pixels(raw_size if isinstance(raw_size, str) else None)
|
||||
match: Final = _SIZE_PATTERN.fullmatch(size or "")
|
||||
return int(match[1]) * int(match[2]) if match else 0
|
||||
|
||||
|
||||
def cost_calculator(
|
||||
|
|
@ -70,13 +58,13 @@ def cost_calculator(
|
|||
num_images: Final = n if n is not None else len(image_response.data or ())
|
||||
generated_meters: Final = (
|
||||
("output_cost_per_image", num_images),
|
||||
("input_cost_per_pixel", _generated_pixels(optional_params, size, image_response) * num_images),
|
||||
("input_cost_per_pixel", _generated_pixels(optional_params, size) * num_images),
|
||||
)
|
||||
generated: Final = next(
|
||||
generated_cost: Final = next(
|
||||
(rate * units for key, units in generated_meters if (rate := _rate(resolved, key)) is not None),
|
||||
0.0,
|
||||
)
|
||||
reference: Final = (_rate(resolved, "input_cost_per_reference_pixel") or 0.0) * (
|
||||
reference_cost: Final = (_rate(resolved, "input_cost_per_reference_pixel") or 0.0) * (
|
||||
image_response.reference_pixels or 0
|
||||
)
|
||||
return generated + reference
|
||||
return generated_cost + reference_cost
|
||||
|
|
|
|||
|
|
@ -69,45 +69,16 @@ def _webp_chunk(fourcc: bytes, body: bytes) -> bytes:
|
|||
return b"RIFF" + struct.pack("<I", len(chunk) + 4) + b"WEBP" + chunk
|
||||
|
||||
|
||||
JPEG_APP0: Final = 0xE0
|
||||
JPEG_APP2_MARKER: Final = 0xE2
|
||||
JPEG_DHT: Final = 0xC4
|
||||
JPEG_JPG_MARKER: Final = 0xC8
|
||||
JPEG_DAC_MARKER: Final = 0xCC
|
||||
JPEG_SOF0_MARKER: Final = 0xC0
|
||||
JPEG_SOF2_MARKER: Final = 0xC2
|
||||
def _fill_jpeg(fill_bytes: int, width: int = 320, height: int = 200) -> bytes:
|
||||
return JPEG_SOI + b"\xff" * fill_bytes + _jpeg_sof_segment(0xC0, width, height)
|
||||
|
||||
|
||||
def _segments_jpeg(marker: int, count: int, payload_size: int = 4) -> bytes:
|
||||
return JPEG_SOI + _jpeg_segment(marker, bytes(payload_size)) * count + _jpeg_sof_segment(0xC0, 320, 200)
|
||||
|
||||
PADDED_JPEG: Final = JPEG_SOI + b"\xff\xff" + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
SHORT_LENGTH_JPEG: Final = JPEG_SOI + bytes((0xFF, JPEG_APP0, 0x00, 0x01))
|
||||
TRUNCATED_SOF_JPEG: Final = JPEG_SOI + bytes((0xFF, JPEG_SOF0_MARKER, 0x00, 0x11, 0x08))
|
||||
SEGMENTS_63_JPEG: Final = (
|
||||
JPEG_SOI + _jpeg_segment(JPEG_APP0, bytes(4)) * 63 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
)
|
||||
SEGMENTS_65_JPEG: Final = (
|
||||
JPEG_SOI + _jpeg_segment(JPEG_APP0, bytes(4)) * 65 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
)
|
||||
SEGMENTS_300_JPEG: Final = (
|
||||
JPEG_SOI + _jpeg_segment(JPEG_APP2_MARKER, bytes(4)) * 300 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
)
|
||||
SEGMENTS_1030_JPEG: Final = (
|
||||
JPEG_SOI + _jpeg_segment(JPEG_APP0, bytes(4)) * 1030 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
)
|
||||
FILL_BYTES_JPEG: Final = JPEG_SOI + b"\xff" * 1100 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
FILL_BEFORE_LARGE_SEGMENT_JPEG: Final = (
|
||||
JPEG_SOI + b"\xff" + _jpeg_segment(JPEG_APP2_MARKER, bytes(0xFEFE)) + _jpeg_sof_segment(JPEG_SOF0_MARKER, 640, 480)
|
||||
)
|
||||
FILL_200K_JPEG: Final = JPEG_SOI + b"\xff" * 200_000 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
FILL_65535_JPEG: Final = JPEG_SOI + b"\xff" * 65535 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
FILL_65536_JPEG: Final = JPEG_SOI + b"\xff" * 65536 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
OVERSIZED_HEADER_JPEG: Final = (
|
||||
JPEG_SOI + _jpeg_segment(JPEG_APP0, bytes(65533)) * 260 + _jpeg_sof_segment(JPEG_SOF0_MARKER, 320, 200)
|
||||
)
|
||||
PROGRESSIVE_JPEG: Final = JPEG_SOI + _jpeg_sof_segment(JPEG_SOF2_MARKER, 111, 55)
|
||||
DHT_JPEG: Final = JPEG_SOI + _jpeg_segment(JPEG_DHT, bytes(8)) + _jpeg_sof_segment(JPEG_SOF0_MARKER, 400, 300)
|
||||
|
||||
VP8_BODY: Final = bytes(3) + b"\x9d\x01\x2a" + struct.pack("<HH", 0xC000 | 500, 0xC000 | 250) + bytes(4)
|
||||
VP8L_BITS: Final = 299 | (199 << 14)
|
||||
VP8L_BODY: Final = b"\x2f" + VP8L_BITS.to_bytes(4, "little") + bytes(5)
|
||||
VP8L_BODY: Final = b"\x2f" + (299 | (199 << 14)).to_bytes(4, "little") + bytes(5)
|
||||
VP8X_BODY: Final = bytes(4) + (700 - 1).to_bytes(3, "little") + (350 - 1).to_bytes(3, "little")
|
||||
|
||||
|
||||
|
|
@ -126,21 +97,33 @@ def _dims(width: int, height: int) -> ImageDimensions:
|
|||
@pytest.mark.parametrize(
|
||||
("header", "expected"),
|
||||
(
|
||||
pytest.param(PADDED_JPEG, _dims(320, 200), id="jpeg-sof-after-0xff-padding"),
|
||||
pytest.param(SHORT_LENGTH_JPEG, None, id="jpeg-segment-length-below-2"),
|
||||
pytest.param(TRUNCATED_SOF_JPEG, None, id="jpeg-truncated-sof-payload"),
|
||||
pytest.param(SEGMENTS_63_JPEG, _dims(320, 200), id="jpeg-sof-at-segment-64-boundary"),
|
||||
pytest.param(SEGMENTS_65_JPEG, _dims(320, 200), id="jpeg-65-segments-within-budget"),
|
||||
pytest.param(SEGMENTS_300_JPEG, _dims(320, 200), id="jpeg-300-app2-segments"),
|
||||
pytest.param(SEGMENTS_1030_JPEG, None, id="jpeg-more-than-1024-segments"),
|
||||
pytest.param(FILL_BYTES_JPEG, _dims(320, 200), id="jpeg-1100-fill-bytes-before-sof"),
|
||||
pytest.param(FILL_BEFORE_LARGE_SEGMENT_JPEG, _dims(640, 480), id="jpeg-fill-before-large-segment"),
|
||||
pytest.param(FILL_200K_JPEG, _dims(320, 200), id="jpeg-200k-fill-bytes-before-sof"),
|
||||
pytest.param(FILL_65535_JPEG, _dims(320, 200), id="jpeg-65535-fill-bytes-before-sof"),
|
||||
pytest.param(FILL_65536_JPEG, _dims(320, 200), id="jpeg-65536-fill-bytes-before-sof"),
|
||||
pytest.param(OVERSIZED_HEADER_JPEG, None, id="jpeg-sof-beyond-16mib"),
|
||||
pytest.param(PROGRESSIVE_JPEG, _dims(111, 55), id="jpeg-sof2-progressive"),
|
||||
pytest.param(DHT_JPEG, _dims(400, 300), id="jpeg-dht-skipped-before-sof0"),
|
||||
pytest.param(
|
||||
JPEG_SOI + b"\xff\xff" + _jpeg_sof_segment(0xC0, 320, 200),
|
||||
_dims(320, 200),
|
||||
id="jpeg-sof-after-0xff-padding",
|
||||
),
|
||||
pytest.param(JPEG_SOI + bytes((0xFF, 0xE0, 0x00, 0x01)), None, id="jpeg-segment-length-below-2"),
|
||||
pytest.param(JPEG_SOI + bytes((0xFF, 0xC0, 0x00, 0x11, 0x08)), None, id="jpeg-truncated-sof-payload"),
|
||||
pytest.param(_segments_jpeg(0xE0, 63), _dims(320, 200), id="jpeg-sof-at-segment-64-boundary"),
|
||||
pytest.param(_segments_jpeg(0xE0, 65), _dims(320, 200), id="jpeg-65-segments-within-budget"),
|
||||
pytest.param(_segments_jpeg(0xE2, 300), _dims(320, 200), id="jpeg-300-app2-segments"),
|
||||
pytest.param(_segments_jpeg(0xE0, 1030), None, id="jpeg-more-than-1024-segments"),
|
||||
pytest.param(_fill_jpeg(1100), _dims(320, 200), id="jpeg-1100-fill-bytes-before-sof"),
|
||||
pytest.param(
|
||||
JPEG_SOI + b"\xff" + _jpeg_segment(0xE2, bytes(0xFEFE)) + _jpeg_sof_segment(0xC0, 640, 480),
|
||||
_dims(640, 480),
|
||||
id="jpeg-fill-before-large-segment",
|
||||
),
|
||||
pytest.param(_fill_jpeg(200_000), _dims(320, 200), id="jpeg-200k-fill-bytes-before-sof"),
|
||||
pytest.param(_fill_jpeg(65535), _dims(320, 200), id="jpeg-65535-fill-bytes-before-sof"),
|
||||
pytest.param(_fill_jpeg(65536), _dims(320, 200), id="jpeg-65536-fill-bytes-before-sof"),
|
||||
pytest.param(_segments_jpeg(0xE0, 260, payload_size=65533), None, id="jpeg-sof-beyond-16mib"),
|
||||
pytest.param(JPEG_SOI + _jpeg_sof_segment(0xC2, 111, 55), _dims(111, 55), id="jpeg-sof2-progressive"),
|
||||
pytest.param(
|
||||
JPEG_SOI + _jpeg_segment(0xC4, bytes(8)) + _jpeg_sof_segment(0xC0, 400, 300),
|
||||
_dims(400, 300),
|
||||
id="jpeg-dht-skipped-before-sof0",
|
||||
),
|
||||
pytest.param(_webp_chunk(b"VP8 ", VP8_BODY), _dims(500, 250), id="webp-vp8-lossy"),
|
||||
pytest.param(_webp_chunk(b"VP8L", VP8L_BODY), _dims(300, 200), id="webp-vp8l-lossless"),
|
||||
pytest.param(_webp_chunk(b"VP8X", VP8X_BODY), _dims(700, 350), id="webp-vp8x-extended"),
|
||||
|
|
@ -236,9 +219,9 @@ def test_read_image_dimensions_reads_every_jpeg_sof_marker(sof_marker: int):
|
|||
assert read_image_dimensions(JPEG_SOI + _jpeg_sof_segment(sof_marker, 320, 200)) == _dims(320, 200)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("skipped_marker", (JPEG_JPG_MARKER, JPEG_DAC_MARKER))
|
||||
@pytest.mark.parametrize("skipped_marker", (0xC8, 0xCC))
|
||||
def test_read_image_dimensions_skips_non_sof_c_range_markers(skipped_marker: int):
|
||||
jpeg: Final = JPEG_SOI + _jpeg_segment(skipped_marker, bytes(8)) + _jpeg_sof_segment(JPEG_SOF0_MARKER, 400, 300)
|
||||
jpeg: Final = JPEG_SOI + _jpeg_segment(skipped_marker, bytes(8)) + _jpeg_sof_segment(0xC0, 400, 300)
|
||||
|
||||
assert read_image_dimensions(jpeg) == _dims(400, 300)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue