from __future__ import annotations from io import StringIO from pathlib import Path from PIL import Image import pytest from rich.console import Console from typer.testing import CliRunner import kneiff.cli.image as cli_image from kneiff.utils.image import concat as concat_utils from kneiff.utils.image import png2jpg as png2jpg_utils from kneiff.progress import ProgressUpdate from tests._cli_helpers import invoke_cli def test_png2jpg_composites_alpha_and_writes_subdirectory(tmp_path: Path) -> None: """Cover PNG alpha handling and destination routing.""" image_path = tmp_path / "source.png" image = Image.new("RGBA", (2, 1), color=(255, 0, 0, 128)) image.save(image_path) exit_code = png2jpg_utils.png2jpg( tmp_path, out_subdir="jpg", quality=95, subsampling=0, bg="#ffffff", ) jpg_path = tmp_path / "jpg" / "source.jpg" assert exit_code == 0 assert jpg_path.exists() with Image.open(jpg_path) as converted: assert converted.mode == "RGB" red, green, blue = converted.getpixel((0, 0)) assert red > green assert red > blue def test_png2jpg_dry_run_does_not_write_files(tmp_path: Path) -> None: """Ensure dry-run mode reports success without touching output.""" image_path = tmp_path / "source.png" Image.new("RGB", (2, 1), color="red").save(image_path) exit_code = png2jpg_utils.png2jpg(tmp_path, dry_run=True) assert exit_code == 0 assert not (tmp_path / "jpg" / "source.jpg").exists() def test_convert_png_jobs_reports_every_status_and_progress(tmp_path: Path) -> None: converted_source = tmp_path / "converted.png" skipped_source = tmp_path / "skipped.png" other_source = tmp_path / "other.png" for path in (converted_source, skipped_source, other_source): Image.new("RGB", (2, 1), color="red").save(path) jobs = png2jpg_utils.discover_png_conversion_jobs(tmp_path, out_subdir="jpg") skipped_output = tmp_path / "jpg" / "skipped.jpg" skipped_output.parent.mkdir() Image.new("RGB", (2, 1), color="blue").save(skipped_output) updates: list[ProgressUpdate] = [] results = png2jpg_utils.convert_png_jobs( jobs, quality=95, subsampling=0, bg_rgb=(255, 255, 255), overwrite=False, dry_run=False, progress_callback=updates.append, ) assert [result.status for result in results] == [ "converted", "converted", "skipped", ] assert len(updates) == 3 assert sum(update.advance for update in updates) == 3 def test_img_png2jpg_redirected_output_keeps_legacy_lines( tmp_path: Path, isolated_cli_project: Path, ) -> None: del isolated_cli_project source = tmp_path / "source.png" Image.new("RGB", (2, 1), color="red").save(source) result = invoke_cli(CliRunner(), ["img", "png2jpg", str(tmp_path)]) assert result.exit_code == 0, result.output assert f"[ok] {source} -> {tmp_path / 'jpg' / 'source.jpg'}" in result.output assert "\x1b[" not in result.output def test_img_png2jpg_terminal_replaces_item_lines_with_progress( monkeypatch: pytest.MonkeyPatch, tmp_path: Path, isolated_cli_project: Path, ) -> None: del isolated_cli_project source = tmp_path / "source.png" Image.new("RGB", (2, 1), color="red").save(source) terminal_output = StringIO() console = Console( file=terminal_output, force_terminal=True, color_system=None, width=100, ) monkeypatch.setattr(cli_image, "stderr_progress_console", lambda: console) result = invoke_cli(CliRunner(), ["img", "png2jpg", str(tmp_path)]) assert result.exit_code == 0, result.output assert "[ok]" not in result.output assert "PNG conversion complete" in terminal_output.getvalue() assert "1/1" in terminal_output.getvalue() def test_concat_imgs_scales_to_exact_width(tmp_path: Path) -> None: """Cover horizontal stacking and rounding drift correction.""" left = tmp_path / "left.png" right = tmp_path / "right.png" output = tmp_path / "collage.png" Image.new("RGBA", (10, 5), color=(255, 0, 0, 255)).save(left) Image.new("RGBA", (20, 10), color=(0, 255, 0, 255)).save(right) result = concat_utils.concat_imgs( [left, right], output=output, width=33, gap=3, bg="transparent", align="bottom", ) assert result == output with Image.open(output) as collage: assert collage.size == (33, 10)