Spaces:
Paused
Paused
| """Regression tests for arbitrary page-range rendering + output-only-translated. | |
| The render layer used to conflate the compacted-list enumerate index with the | |
| original page number, so any range not starting at page 0 silently rendered | |
| nothing. These tests pin the fixed behavior: | |
| - a range like [2, 3, 4] (not starting at 0) renders correctly, and | |
| - the output PDF contains ONLY the translated pages, in order. | |
| """ | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| import fitz | |
| import pytest | |
| sys.path.insert(0, str(Path(__file__).parent.parent)) | |
| from pdf2zh.render.config import RenderConfig | |
| from pdf2zh.render.renderer import render_document | |
| TYPST = shutil.which("typst") | |
| def _make_pdf(path: Path, n_pages: int) -> None: | |
| doc = fitz.open() | |
| for i in range(n_pages): | |
| page = doc.new_page(width=300, height=200) | |
| page.insert_text((20, 40), f"Original page {i} native text") | |
| doc.save(str(path)) | |
| doc.close() | |
| def _compacted_parsed(page_indices: list[int]) -> dict: | |
| """A parsed doc compacted to `page_indices` (mirrors parse_pdf(pages=...)).""" | |
| return { | |
| "pages": [ | |
| { | |
| "page_index": pi, | |
| "page_width": 300, | |
| "page_height": 200, | |
| "elements": [ | |
| { | |
| "category": "FLOWING_TEXT", | |
| "label": "Text", | |
| "bbox_pdf": [20, 30, 280, 55], | |
| "source_text": f"Original page {pi} native text", | |
| "translated_text": f"Trang dịch số {pi}", | |
| } | |
| ], | |
| } | |
| for pi in page_indices | |
| ] | |
| } | |
| class TestArbitraryPageRange: | |
| def test_midrange_renders_and_outputs_only_selected(self, tmp_path): | |
| pdf_path = tmp_path / "src.pdf" | |
| _make_pdf(pdf_path, n_pages=6) | |
| pages = [2, 3, 4] # 0-based, NOT starting at 0 | |
| parsed = _compacted_parsed(pages) | |
| cfg = RenderConfig(typst_binary=TYPST) | |
| cfg.pages = pages | |
| out_pdf = tmp_path / "out.pdf" | |
| stats = render_document(pdf_path, parsed, out_pdf, cfg) | |
| # Rendered all 3 selected pages (the pre-fix bug rendered 0). | |
| assert stats["pages"] == 3 | |
| assert stats["elements_rendered"] == 3 | |
| # Output contains ONLY the translated pages, not the full 6-page doc. | |
| out = fitz.open(str(out_pdf)) | |
| try: | |
| assert out.page_count == 3 | |
| # Translated text present; the out-of-range page-0 text must be gone. | |
| assert "Trang dịch số 2" in out[0].get_text() | |
| assert "Original page 0" not in out[0].get_text() | |
| finally: | |
| out.close() | |
| def test_pages_none_keeps_all_pages(self, tmp_path): | |
| pdf_path = tmp_path / "src.pdf" | |
| _make_pdf(pdf_path, n_pages=3) | |
| parsed = _compacted_parsed([0, 1, 2]) | |
| cfg = RenderConfig(typst_binary=TYPST) | |
| cfg.pages = None # translate whole document | |
| out_pdf = tmp_path / "out.pdf" | |
| render_document(pdf_path, parsed, out_pdf, cfg) | |
| out = fitz.open(str(out_pdf)) | |
| try: | |
| assert out.page_count == 3 | |
| finally: | |
| out.close() | |
| _PARA = ( | |
| "This paragraph is long enough in the source that autofit picks a modest " | |
| "font size and the box is treated as multi line rather than single line." | |
| ) | |
| _LONG = _PARA + " " + _PARA + " " + _PARA # translated overflows the tight box | |
| def _para_span_stats(pdf: Path): | |
| doc = fitz.open(str(pdf)) | |
| try: | |
| sizes, bottoms = [], [] | |
| for b in doc[0].get_text("dict")["blocks"]: | |
| for line in b.get("lines", []): | |
| for s in line["spans"]: | |
| if "paragraph" in s["text"] or "multi" in s["text"]: | |
| sizes.append(s["size"]) | |
| bottoms.append(s["bbox"][3]) | |
| return (max(sizes) if sizes else 0.0), (max(bottoms) if bottoms else 0.0) | |
| finally: | |
| doc.close() | |
| class TestCollisionAwareSizing: | |
| """Text keeps its size and overflows into empty space, but shrinks to avoid | |
| colliding with a neighbor below (mirrors the sizing heuristic).""" | |
| def _render(self, tmp_path, with_neighbor): | |
| pdf_path = tmp_path / f"src_{with_neighbor}.pdf" | |
| doc = fitz.open() | |
| doc.new_page(width=400, height=400) | |
| doc.save(str(pdf_path)) | |
| doc.close() | |
| els = [ | |
| { | |
| "label": "Text", | |
| "category": "FLOWING_TEXT", | |
| "bbox_pdf": [40, 40, 360, 110], | |
| "source_text": _PARA, | |
| "translated_text": _LONG, | |
| "cells": [], | |
| } | |
| ] | |
| if with_neighbor: | |
| els.append( | |
| { | |
| "label": "Text", | |
| "category": "FLOWING_TEXT", | |
| "bbox_pdf": [40, 116, 360, 150], | |
| "source_text": "N", | |
| "translated_text": "Neighbor line", | |
| "cells": [], | |
| } | |
| ) | |
| parsed = { | |
| "pages": [ | |
| { | |
| "page_index": 0, | |
| "page_width": 400, | |
| "page_height": 400, | |
| "elements": els, | |
| } | |
| ] | |
| } | |
| out = tmp_path / f"out_{with_neighbor}.pdf" | |
| render_document(pdf_path, parsed, out, RenderConfig(typst_binary=TYPST)) | |
| return _para_span_stats(out) | |
| def test_no_neighbor_keeps_larger_size_and_overflows_down(self, tmp_path): | |
| size_free, bottom_free = self._render(tmp_path, with_neighbor=False) | |
| size_near, _ = self._render(tmp_path, with_neighbor=True) | |
| # Empty space below → keeps a bigger font than when a neighbor forces a shrink. | |
| assert size_free > size_near + 0.5 | |
| # And the text is allowed to overflow below the tight bbox (y1 = 110). | |
| assert bottom_free > 110 | |
| _LONG_TITLE = ( | |
| "A very long translated chapter title that would run past the page number " | |
| "column if it were allowed to expand all the way to the right page margin" | |
| ) | |
| def _title_right_edge(self, tmp_path, with_number): | |
| pdf_path = tmp_path / f"toc_{with_number}.pdf" | |
| doc = fitz.open() | |
| doc.new_page(width=595, height=842) | |
| doc.save(str(pdf_path)) | |
| doc.close() | |
| els = [ | |
| { | |
| "label": "Text", | |
| "category": "FLOWING_TEXT", | |
| "bbox_pdf": [80, 40, 290, 56], # single-line title | |
| "source_text": "Short", | |
| "translated_text": self._LONG_TITLE, | |
| "cells": [], | |
| } | |
| ] | |
| if with_number: | |
| els.append( | |
| { | |
| "label": "Text", | |
| "category": "FLOWING_TEXT", | |
| "bbox_pdf": [527, 40, 543, 53], # page number, same row | |
| "source_text": "ii", | |
| "translated_text": "ii", | |
| "cells": [], | |
| } | |
| ) | |
| parsed = { | |
| "pages": [ | |
| { | |
| "page_index": 0, | |
| "page_width": 595, | |
| "page_height": 842, | |
| "elements": els, | |
| } | |
| ] | |
| } | |
| out = tmp_path / f"toc_out_{with_number}.pdf" | |
| render_document(pdf_path, parsed, out, RenderConfig(typst_binary=TYPST)) | |
| doc = fitz.open(str(out)) | |
| try: | |
| right = 0.0 | |
| for b in doc[0].get_text("dict")["blocks"]: | |
| for line in b.get("lines", []): | |
| for s in line["spans"]: | |
| if s["text"].strip() != "ii": | |
| right = max(right, s["bbox"][2]) | |
| return right | |
| finally: | |
| doc.close() | |
| def test_single_line_title_stops_before_right_neighbor(self, tmp_path): | |
| with_num = self._title_right_edge(tmp_path, with_number=True) | |
| without_num = self._title_right_edge(tmp_path, with_number=False) | |
| # With a page number on the right, the title must not overrun its left edge. | |
| assert with_num <= 527.5 | |
| # Without it, the title is free to use the rest of the page width. | |
| assert without_num > with_num | |
| class TestColorOverride: | |
| """User-added boxes may carry explicit bg/text colors (review.add_element).""" | |
| def test_sample_colors_honors_element_overrides(self, tmp_path): | |
| from pdf2zh.render.renderer import _sample_colors | |
| pdf = tmp_path / "p.pdf" | |
| _make_pdf(pdf, 1) | |
| parsed = { | |
| "pages": [ | |
| { | |
| "page_index": 0, | |
| "page_width": 300, | |
| "page_height": 200, | |
| "elements": [ | |
| { | |
| "category": "FLOWING_TEXT", | |
| "label": "Text", | |
| "bbox_pdf": [20, 30, 120, 55], | |
| "source_text": "x", | |
| "translated_text": "y", | |
| "bg_color": [10, 20, 30], | |
| "text_color": [200, 100, 50], | |
| } | |
| ], | |
| } | |
| ] | |
| } | |
| bg_colors: dict = {} | |
| text_colors: dict = {} | |
| stats = {"bg_samples": 0} | |
| _sample_colors(pdf, parsed, RenderConfig(), {}, bg_colors, text_colors, stats) | |
| # Override used verbatim — no sampling from the page pixels. | |
| assert bg_colors["p0:e0"] == (10, 20, 30) | |
| assert text_colors["p0:e0"] == (200, 100, 50) | |