From 22027ebea57e2adbc27190bb638161c0a4c74353 Mon Sep 17 00:00:00 2001 From: DyNooob Date: Mon, 22 Jun 2026 14:09:05 +0800 Subject: [PATCH 1/2] feat(cli): add batch URL input support --- render/src/pixelrag_render/render.py | 51 ++++++++++++++-------------- tests/test_cli.py | 37 ++++++++++++++++++++ 2 files changed, 63 insertions(+), 25 deletions(-) diff --git a/render/src/pixelrag_render/render.py b/render/src/pixelrag_render/render.py index 35a4753..8c7d8d1 100644 --- a/render/src/pixelrag_render/render.py +++ b/render/src/pixelrag_render/render.py @@ -193,8 +193,8 @@ def main() -> None: # Single URL, default CDP backend pixelshot https://example.com --output ./tiles - # Multiple inputs with 4 workers - pixelshot https://a.com https://b.com --output ./tiles --workers 4 + # Multiple inputs + pixelshot https://a.com https://b.com --output ./tiles # PDF pixelshot report.pdf --output ./tiles @@ -202,8 +202,8 @@ def main() -> None: # Local HTML pixelshot index.html --output ./tiles --backend playwright - # Pipe URLs from a file - cat urls.txt | xargs pixelshot --output ./tiles --workers 8 + # URL file + pixelshot urls.txt --output ./tiles # Chrome management (folded from the former `pixelrag-chrome`) pixelshot install-chrome # download the patched headless Chrome @@ -294,36 +294,37 @@ def main() -> None: args = parser.parse_args() output_dir = Path(args.output) - # Partition inputs into URLs and files for batch processing urls = [] files = [] for inp in args.inputs: - if inp.startswith("http://") or inp.startswith("https://"): + if inp.lower().endswith(".txt"): + with open(inp, encoding="utf-8") as f: + for line in f: + url = line.strip() + if url: + urls.append(url) + elif inp.startswith("http://") or inp.startswith("https://"): urls.append(inp) else: files.append(Path(inp)) results: list[Path] = [] - # Batch-render URLs together for efficiency - if urls: - logger.info( - "Rendering %d URL(s) with backend=%s workers=%d", - len(urls), - args.backend, - args.workers, - ) - tile_dirs = render_urls( - urls, - output_dir, - backend=args.backend, - tile_height=args.tile_height, - quality=args.quality, - viewport_width=args.viewport_width, - workers=args.workers, - wait_network_idle=args.wait_network_idle, - ) - results.extend(tile_dirs) + for url in urls: + try: + tile_dirs = render_url( + url, + output_dir, + backend=args.backend, + tile_height=args.tile_height, + quality=args.quality, + viewport_width=args.viewport_width, + workers=args.workers, + wait_network_idle=args.wait_network_idle, + ) + results.extend(tile_dirs) + except Exception as e: + logger.error("Failed to render %s: %s", url, e) # Handle files individually (they may need different backends) for fpath in files: diff --git a/tests/test_cli.py b/tests/test_cli.py index 1c43df2..e738e63 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -23,6 +23,43 @@ def test_pixelshot_help(): assert "pixelshot" in r.stdout +def test_pixelshot_txt_input(monkeypatch, tmp_path, capsys): + from pixelrag_render import render as render_mod + + urls_file = tmp_path / "urls.txt" + urls_file.write_text( + "\n" + " https://example.com/a \n" + "\n" + "https://example.com/b\n" + "https://example.com/c\n" + ) + calls = [] + + def fake_render_url(url, output_dir, **kwargs): + calls.append(url) + if url.endswith("/b"): + raise RuntimeError("boom") + return [Path(output_dir) / f"{url.rsplit('/', 1)[-1]}.png.tiles"] + + monkeypatch.setattr(render_mod, "render_url", fake_render_url) + monkeypatch.setattr( + sys, "argv", ["pixelshot", str(urls_file), "-o", str(tmp_path / "out")] + ) + + render_mod.main() + + assert calls == [ + "https://example.com/a", + "https://example.com/b", + "https://example.com/c", + ] + stdout = capsys.readouterr().out + assert "a.png.tiles" in stdout + assert "b.png.tiles" not in stdout + assert "c.png.tiles" in stdout + + def test_pixelrag_umbrella_help(): r = _run("pixelrag", "--help") assert r.returncode == 0 From 68fcb10622e2cf2aa32303d4cf7cf0e32d198b4c Mon Sep 17 00:00:00 2001 From: DyNooob Date: Tue, 23 Jun 2026 16:06:50 +0800 Subject: [PATCH 2/2] fix(cli): preserve batched URL rendering --- render/src/pixelrag_render/render.py | 46 ++++++++++++++------------ tests/test_cli.py | 48 ++++++++++++++++++---------- 2 files changed, 58 insertions(+), 36 deletions(-) diff --git a/render/src/pixelrag_render/render.py b/render/src/pixelrag_render/render.py index 8c7d8d1..4ac043b 100644 --- a/render/src/pixelrag_render/render.py +++ b/render/src/pixelrag_render/render.py @@ -298,11 +298,14 @@ def main() -> None: files = [] for inp in args.inputs: if inp.lower().endswith(".txt"): - with open(inp, encoding="utf-8") as f: - for line in f: - url = line.strip() - if url: - urls.append(url) + try: + with open(inp, encoding="utf-8") as f: + for line in f: + url = line.strip() + if url: + urls.append(url) + except FileNotFoundError: + parser.error(f"URL file not found: {inp}") elif inp.startswith("http://") or inp.startswith("https://"): urls.append(inp) else: @@ -310,21 +313,24 @@ def main() -> None: results: list[Path] = [] - for url in urls: - try: - tile_dirs = render_url( - url, - output_dir, - backend=args.backend, - tile_height=args.tile_height, - quality=args.quality, - viewport_width=args.viewport_width, - workers=args.workers, - wait_network_idle=args.wait_network_idle, - ) - results.extend(tile_dirs) - except Exception as e: - logger.error("Failed to render %s: %s", url, e) + if urls: + logger.info( + "Rendering %d URL(s) with backend=%s workers=%d", + len(urls), + args.backend, + args.workers, + ) + tile_dirs = render_urls( + urls, + output_dir, + backend=args.backend, + tile_height=args.tile_height, + quality=args.quality, + viewport_width=args.viewport_width, + workers=args.workers, + wait_network_idle=args.wait_network_idle, + ) + results.extend(tile_dirs) # Handle files individually (they may need different backends) for fpath in files: diff --git a/tests/test_cli.py b/tests/test_cli.py index e738e63..a3b7177 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,6 +8,8 @@ import sys from pathlib import Path +import pytest + # Console scripts live next to the interpreter running the tests (works whether # invoked via `uv run pytest` or `.venv/bin/python -m pytest`). _BIN = Path(sys.executable).parent @@ -23,36 +25,37 @@ def test_pixelshot_help(): assert "pixelshot" in r.stdout -def test_pixelshot_txt_input(monkeypatch, tmp_path, capsys): +def test_pixelshot_txt_input_uses_batch_render(monkeypatch, tmp_path, capsys): from pixelrag_render import render as render_mod urls_file = tmp_path / "urls.txt" urls_file.write_text( - "\n" - " https://example.com/a \n" - "\n" - "https://example.com/b\n" - "https://example.com/c\n" + "\n https://example.com/a \n\nhttps://example.com/b\nhttps://example.com/c\n" ) calls = [] - def fake_render_url(url, output_dir, **kwargs): - calls.append(url) - if url.endswith("/b"): - raise RuntimeError("boom") - return [Path(output_dir) / f"{url.rsplit('/', 1)[-1]}.png.tiles"] + def fake_render_urls(urls, output_dir, **kwargs): + calls.append((list(urls), kwargs["workers"])) + return [Path(output_dir) / "a.png.tiles", Path(output_dir) / "c.png.tiles"] - monkeypatch.setattr(render_mod, "render_url", fake_render_url) + monkeypatch.setattr(render_mod, "render_urls", fake_render_urls) monkeypatch.setattr( - sys, "argv", ["pixelshot", str(urls_file), "-o", str(tmp_path / "out")] + sys, + "argv", + ["pixelshot", str(urls_file), "-o", str(tmp_path / "out"), "-w", "8"], ) render_mod.main() assert calls == [ - "https://example.com/a", - "https://example.com/b", - "https://example.com/c", + ( + [ + "https://example.com/a", + "https://example.com/b", + "https://example.com/c", + ], + 8, + ) ] stdout = capsys.readouterr().out assert "a.png.tiles" in stdout @@ -60,6 +63,19 @@ def fake_render_url(url, output_dir, **kwargs): assert "c.png.tiles" in stdout +def test_pixelshot_missing_txt_input(monkeypatch, tmp_path, capsys): + from pixelrag_render import render as render_mod + + missing = tmp_path / "missing.txt" + monkeypatch.setattr(sys, "argv", ["pixelshot", str(missing)]) + + with pytest.raises(SystemExit) as exc: + render_mod.main() + + assert exc.value.code == 2 + assert "URL file not found" in capsys.readouterr().err + + def test_pixelrag_umbrella_help(): r = _run("pixelrag", "--help") assert r.returncode == 0