Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 40 additions & 10 deletions src/newsdom_api/synthetic.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""Synthetic newspaper fixture generation for redistributable repository tests."""
"""Synthetic newspaper fixture generation for redistributable repository tests.

Provides programmatic tools to generate reliable deterministic fixture images, pdfs and truth files.
"""

from __future__ import annotations

Expand All @@ -16,7 +19,10 @@


def _font_candidates() -> list[str]:
"""Return preferred macOS Japanese font candidates for fixture rendering."""
"""Return preferred macOS Japanese font candidates for fixture rendering.

These fonts are used as fallback options to support rendering Japanese text correctly in synthetic images.
"""

return [
"/System/Library/Fonts/ヒラギノ角ゴシック W3.ttc",
Expand All @@ -26,7 +32,10 @@ def _font_candidates() -> list[str]:


def _load_font(size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
"""Load the first available Japanese-capable font at the requested size."""
"""Load the first available Japanese-capable font at the requested size.

Checks availability of the fonts from the candidates list, and loads the first found font. Defaults to standard font.
"""

for candidate in _font_candidates():
if Path(candidate).exists():
Expand All @@ -41,7 +50,10 @@ def _safe_draw_text(
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
fill: int | str = "black",
) -> None:
"""Draw text with a fallback mechanism to prevent UnicodeEncodeError on default fonts."""
"""Draw text with a fallback mechanism to prevent UnicodeEncodeError on default fonts.

Falls back to replacing unsupported unicode characters with question marks if standard drawing fails.
"""
try:
draw.text(xy, text, fill=fill, font=font)
except UnicodeEncodeError:
Expand All @@ -58,7 +70,10 @@ def _draw_vertical_text(
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
line_height: int,
) -> None:
"""Render a string as simple top-to-bottom vertical glyph placement."""
"""Render a string as simple top-to-bottom vertical glyph placement.

Draws characters one by one with a vertical offset calculated by line_height.
"""

cursor_y = y
for char in text:
Expand All @@ -67,7 +82,10 @@ def _draw_vertical_text(


def _split_vertical(text: str, max_chars: int) -> list[str]:
"""Split text into vertical columns constrained by the page height budget."""
"""Split text into vertical columns constrained by the page height budget.

The output list of strings can be mapped to individual vertical columns to fit within a given height limit.
"""

return [text[idx : idx + max_chars] for idx in range(0, len(text), max_chars)]

Expand All @@ -78,7 +96,10 @@ def _draw_vertical_columns(
text: str,
font: ImageFont.FreeTypeFont | ImageFont.ImageFont,
) -> None:
"""Render multiple vertical columns of text inside the supplied bounding box."""
"""Render multiple vertical columns of text inside the supplied bounding box.

The text content is first split into fitting columns, and each column is sequentially rendered.
"""

x0, y0, x1, y1 = bbox
font_size = int(getattr(font, "size", 24))
Expand All @@ -101,7 +122,10 @@ def _article_block(
vertical: bool = True,
page_number: int = 1,
) -> dict:
"""Create one synthetic article descriptor for the fixture ground truth."""
"""Create one synthetic article descriptor for the fixture ground truth.

The synthetic descriptor is primarily used to build validation metrics for parsing tasks.
"""

return {
"headline": headline,
Expand All @@ -113,7 +137,10 @@ def _article_block(


def _ground_truth() -> dict:
"""Return the deterministic article/image/ad structure used for the fixture."""
"""Return the deterministic article/image/ad structure used for the fixture.

The output includes predefined structure configuration blocks such as texts, layouts, and page parameters.
"""

return {
"page_size": [PAGE_WIDTH, PAGE_HEIGHT],
Expand Down Expand Up @@ -160,7 +187,10 @@ def _ground_truth() -> dict:


def generate_fixture(output_dir: Path, seed: int = 7) -> tuple[Path, Path]:
"""Generate a synthetic scanned-newspaper PDF fixture and ground-truth JSON."""
"""Generate a synthetic scanned-newspaper PDF fixture and ground-truth JSON.

A deterministic PDF and JSON structure configuration will be stored inside the target `output_dir` parameter.
Comment on lines +190 to +192

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 결정성을 과장하는 문서

seed는 파일명만 바꾸고 PDF 메타데이터를 고정하지 않습니다. 새 docstring의 결정성 보장은 바이트 단위 재현성으로 오해될 수 있습니다.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"""

resolved_dir = output_dir.resolve()
try:
Expand Down
13 changes: 13 additions & 0 deletions tests/test_tools_export_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,16 @@ def test_export_csv_cli_invalid_file(
assert exc_info.value.code == 1
captured = capsys.readouterr()
assert "Error exporting CSV:" in captured.err


def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None:
import runpy
import sys
from unittest.mock import patch

monkeypatch.delitem(sys.modules, "tools.export_csv", raising=False)
with patch("sys.argv", ["tools/export_csv.py", "-h"]):
with pytest.raises(SystemExit) as excinfo:
runpy.run_module("tools.export_csv", run_name="__main__")

assert excinfo.value.code == 0
13 changes: 13 additions & 0 deletions tests/test_tools_export_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,16 @@ def test_main_file_output_error(

assert excinfo.value.code == 1
assert "Error exporting HTML" in capsys.readouterr().err


def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None:
import runpy
import sys
from unittest.mock import patch

monkeypatch.delitem(sys.modules, "tools.export_html", raising=False)
with patch("sys.argv", ["tools/export_html.py", "-h"]):
with pytest.raises(SystemExit) as excinfo:
runpy.run_module("tools.export_html", run_name="__main__")

assert excinfo.value.code == 0
13 changes: 13 additions & 0 deletions tests/test_tools_export_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,16 @@ def test_main_file_output_error(tmp_path, sample_json_data, capsys):

assert excinfo.value.code == 1
assert "Error exporting Markdown" in capsys.readouterr().err


def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None:
import runpy
import sys
from unittest.mock import patch

monkeypatch.delitem(sys.modules, "tools.export_markdown", raising=False)
with patch("sys.argv", ["tools/export_markdown.py", "-h"]):
with pytest.raises(SystemExit) as excinfo:
runpy.run_module("tools.export_markdown", run_name="__main__")

assert excinfo.value.code == 0
13 changes: 13 additions & 0 deletions tests/test_tools_extract_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,16 @@ def test_extract_text_wrong_ext(tmp_path, capsys):
extract_text.main([str(txt)])
assert e.value.code == 1
assert "must be a .json file" in capsys.readouterr().err


def test_module_main(monkeypatch: pytest.MonkeyPatch) -> None:
import runpy
import sys
from unittest.mock import patch

monkeypatch.delitem(sys.modules, "tools.extract_text", raising=False)
with patch("sys.argv", ["tools/extract_text.py", "-h"]):
with pytest.raises(SystemExit) as excinfo:
runpy.run_module("tools.extract_text", run_name="__main__")

assert excinfo.value.code == 0
2 changes: 1 addition & 1 deletion tools/export_csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,5 @@ def main(argv: list[str] | None = None) -> None:
sys.exit(1)


if __name__ == "__main__": # pragma: no cover
if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion tools/export_html.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,5 +161,5 @@ def main(argv: list[str] | None = None) -> None:
sys.exit(1)


if __name__ == "__main__": # pragma: no cover
if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion tools/export_markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,5 @@ def main(argv: list[str] | None = None) -> None:
sys.exit(1)


if __name__ == "__main__": # pragma: no cover
if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion tools/extract_text.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,5 @@ def main(argv: list[str] | None = None) -> None:
sys.exit(1)


if __name__ == "__main__": # pragma: no cover
if __name__ == "__main__":
main()
Loading