- Add hOCR test fixtures for Latin, Arabic, CJK, Devanagari scripts - Add tests for fpdf2 renderer, multi-font manager, system font provider - Add multilingual rendering tests - Update existing tests to use fpdf2 renderer
367 lines
12 KiB
Python
367 lines
12 KiB
Python
# SPDX-FileCopyrightText: 2025 James R. Barlow
|
|
# SPDX-License-Identifier: MPL-2.0
|
|
|
|
"""Tests for fpdf2-based PDF renderer."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from ocrmypdf.font import MultiFontManager
|
|
from ocrmypdf.fpdf_renderer import DebugRenderOptions, Fpdf2MultiPageRenderer, Fpdf2PdfRenderer
|
|
from ocrmypdf.hocrtransform.hocr_parser import HocrParser
|
|
from ocrmypdf.hocrtransform.ocr_element import OcrClass
|
|
|
|
|
|
@pytest.fixture
|
|
def font_dir():
|
|
"""Return path to font directory."""
|
|
return Path(__file__).parent.parent / "src" / "ocrmypdf" / "data"
|
|
|
|
|
|
@pytest.fixture
|
|
def multi_font_manager(font_dir):
|
|
"""Create MultiFontManager instance for testing."""
|
|
return MultiFontManager(font_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def resources():
|
|
"""Return path to test resources directory."""
|
|
return Path(__file__).parent / "resources"
|
|
|
|
|
|
class TestFpdf2RendererImports:
|
|
"""Test that all fpdf2 renderer modules can be imported."""
|
|
|
|
def test_imports(self):
|
|
"""Test that all fpdf_renderer modules can be imported."""
|
|
from ocrmypdf.fpdf_renderer import (
|
|
DebugRenderOptions,
|
|
Fpdf2MultiPageRenderer,
|
|
Fpdf2PdfRenderer,
|
|
)
|
|
assert DebugRenderOptions is not None
|
|
assert Fpdf2PdfRenderer is not None
|
|
assert Fpdf2MultiPageRenderer is not None
|
|
|
|
|
|
class TestDebugRenderOptions:
|
|
"""Test DebugRenderOptions dataclass."""
|
|
|
|
def test_defaults(self):
|
|
"""Test default values."""
|
|
opts = DebugRenderOptions()
|
|
assert opts.render_baseline is False
|
|
assert opts.render_line_bbox is False
|
|
assert opts.render_word_bbox is False
|
|
|
|
def test_custom_values(self):
|
|
"""Test custom values."""
|
|
opts = DebugRenderOptions(
|
|
render_baseline=True,
|
|
render_line_bbox=True,
|
|
render_word_bbox=True,
|
|
)
|
|
assert opts.render_baseline is True
|
|
assert opts.render_line_bbox is True
|
|
assert opts.render_word_bbox is True
|
|
|
|
|
|
class TestFpdf2PdfRenderer:
|
|
"""Test Fpdf2PdfRenderer."""
|
|
|
|
def test_requires_page_element(self, multi_font_manager):
|
|
"""Test that renderer requires ocr_page element."""
|
|
from ocrmypdf.hocrtransform.ocr_element import BoundingBox, OcrElement
|
|
|
|
# Create a non-page element
|
|
word = OcrElement(
|
|
ocr_class=OcrClass.WORD,
|
|
text="test",
|
|
bbox=BoundingBox(left=0, top=0, right=100, bottom=20),
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="Root element must be ocr_page"):
|
|
Fpdf2PdfRenderer(
|
|
page=word,
|
|
dpi=300,
|
|
multi_font_manager=multi_font_manager,
|
|
)
|
|
|
|
def test_requires_bbox(self, multi_font_manager):
|
|
"""Test that renderer requires page with bounding box."""
|
|
from ocrmypdf.hocrtransform.ocr_element import OcrElement
|
|
|
|
page = OcrElement(ocr_class=OcrClass.PAGE)
|
|
|
|
with pytest.raises(ValueError, match="Page must have bounding box"):
|
|
Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=300,
|
|
multi_font_manager=multi_font_manager,
|
|
)
|
|
|
|
def test_render_simple_page(self, multi_font_manager, tmp_path):
|
|
"""Test rendering a simple page with one word."""
|
|
from ocrmypdf.hocrtransform.ocr_element import BoundingBox, OcrElement
|
|
|
|
# Create a simple page with one word
|
|
word = OcrElement(
|
|
ocr_class=OcrClass.WORD,
|
|
text="Hello",
|
|
bbox=BoundingBox(left=100, top=100, right=200, bottom=130),
|
|
)
|
|
line = OcrElement(
|
|
ocr_class=OcrClass.LINE,
|
|
bbox=BoundingBox(left=100, top=100, right=200, bottom=130),
|
|
children=[word],
|
|
)
|
|
page = OcrElement(
|
|
ocr_class=OcrClass.PAGE,
|
|
bbox=BoundingBox(left=0, top=0, right=612, bottom=792),
|
|
children=[line],
|
|
)
|
|
|
|
renderer = Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=72, # 1:1 mapping to PDF points
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "test_simple.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
def test_render_invisible_text(self, multi_font_manager, tmp_path):
|
|
"""Test rendering invisible text (OCR layer)."""
|
|
from ocrmypdf.hocrtransform.ocr_element import BoundingBox, OcrElement
|
|
|
|
word = OcrElement(
|
|
ocr_class=OcrClass.WORD,
|
|
text="Invisible",
|
|
bbox=BoundingBox(left=100, top=100, right=250, bottom=130),
|
|
)
|
|
line = OcrElement(
|
|
ocr_class=OcrClass.LINE,
|
|
bbox=BoundingBox(left=100, top=100, right=250, bottom=130),
|
|
children=[word],
|
|
)
|
|
page = OcrElement(
|
|
ocr_class=OcrClass.PAGE,
|
|
bbox=BoundingBox(left=0, top=0, right=612, bottom=792),
|
|
children=[line],
|
|
)
|
|
|
|
renderer = Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=72,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=True, # This is the default
|
|
)
|
|
|
|
output_path = tmp_path / "test_invisible.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
|
|
class TestFpdf2MultiPageRenderer:
|
|
"""Test Fpdf2MultiPageRenderer."""
|
|
|
|
def test_requires_pages(self, multi_font_manager):
|
|
"""Test that renderer requires at least one page."""
|
|
with pytest.raises(ValueError, match="No pages to render"):
|
|
renderer = Fpdf2MultiPageRenderer(
|
|
pages_data=[],
|
|
multi_font_manager=multi_font_manager,
|
|
)
|
|
renderer.render(Path("/tmp/test.pdf"))
|
|
|
|
def test_render_multiple_pages(self, multi_font_manager, tmp_path):
|
|
"""Test rendering multiple pages."""
|
|
from ocrmypdf.hocrtransform.ocr_element import BoundingBox, OcrElement
|
|
|
|
pages_data = []
|
|
for i in range(3):
|
|
word = OcrElement(
|
|
ocr_class=OcrClass.WORD,
|
|
text=f"Page{i+1}",
|
|
bbox=BoundingBox(left=100, top=100, right=200, bottom=130),
|
|
)
|
|
line = OcrElement(
|
|
ocr_class=OcrClass.LINE,
|
|
bbox=BoundingBox(left=100, top=100, right=200, bottom=130),
|
|
children=[word],
|
|
)
|
|
page = OcrElement(
|
|
ocr_class=OcrClass.PAGE,
|
|
bbox=BoundingBox(left=0, top=0, right=612, bottom=792),
|
|
children=[line],
|
|
)
|
|
pages_data.append((i + 1, page, 72))
|
|
|
|
renderer = Fpdf2MultiPageRenderer(
|
|
pages_data=pages_data,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "test_multipage.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
|
|
class TestFpdf2RendererWithHocr:
|
|
"""Test fpdf2 renderer with actual hOCR files."""
|
|
|
|
def test_render_latin_hocr(self, resources, multi_font_manager, tmp_path):
|
|
"""Test rendering Latin text from hOCR."""
|
|
hocr_path = resources / "latin.hocr"
|
|
if not hocr_path.exists():
|
|
pytest.skip("latin.hocr not found")
|
|
|
|
parser = HocrParser(hocr_path)
|
|
page = parser.parse()
|
|
|
|
# Ensure we got a page
|
|
assert page.ocr_class == OcrClass.PAGE
|
|
assert page.bbox is not None
|
|
|
|
renderer = Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=300,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "latin_fpdf2.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
def test_render_cjk_hocr(self, resources, multi_font_manager, tmp_path):
|
|
"""Test rendering CJK text from hOCR."""
|
|
hocr_path = resources / "cjk.hocr"
|
|
if not hocr_path.exists():
|
|
pytest.skip("cjk.hocr not found")
|
|
|
|
parser = HocrParser(hocr_path)
|
|
page = parser.parse()
|
|
|
|
renderer = Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=300,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "cjk_fpdf2.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
def test_render_arabic_hocr(self, resources, multi_font_manager, tmp_path):
|
|
"""Test rendering Arabic text from hOCR."""
|
|
hocr_path = resources / "arabic.hocr"
|
|
if not hocr_path.exists():
|
|
pytest.skip("arabic.hocr not found")
|
|
|
|
parser = HocrParser(hocr_path)
|
|
page = parser.parse()
|
|
|
|
renderer = Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=300,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "arabic_fpdf2.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
def test_render_hello_world_scripts_hocr(self, resources, multi_font_manager, tmp_path):
|
|
"""Test rendering comprehensive multilingual 'Hello!' hOCR file.
|
|
|
|
This tests all major scripts including:
|
|
- Latin (English, Spanish, French, German, Italian, Polish, Portuguese, Turkish)
|
|
- Cyrillic (Russian)
|
|
- Greek
|
|
- CJK (Chinese Simplified, Chinese Traditional, Japanese, Korean)
|
|
- Devanagari (Hindi)
|
|
- Arabic (RTL)
|
|
- Hebrew (RTL)
|
|
|
|
Also includes rotated baselines to exercise skew handling.
|
|
"""
|
|
hocr_path = resources / "hello_world_scripts.hocr"
|
|
if not hocr_path.exists():
|
|
pytest.skip("hello_world_scripts.hocr not found")
|
|
|
|
parser = HocrParser(hocr_path)
|
|
page = parser.parse()
|
|
|
|
# Verify we parsed the page correctly
|
|
assert page.ocr_class == OcrClass.PAGE
|
|
assert page.bbox is not None
|
|
# Should have 2550x3300 at 300 DPI
|
|
assert page.bbox.right == 2550
|
|
assert page.bbox.bottom == 3300
|
|
|
|
# Test with visible text for visual inspection
|
|
renderer = Fpdf2PdfRenderer(
|
|
page=page,
|
|
dpi=300,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "hello_world_scripts_fpdf2.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|
|
|
|
def test_render_hello_world_scripts_multipage(
|
|
self, resources, multi_font_manager, tmp_path
|
|
):
|
|
"""Test rendering hello_world_scripts.hocr using MultiPageRenderer.
|
|
|
|
Uses Fpdf2MultiPageRenderer to render the multilingual test file,
|
|
demonstrating font handling across all major writing systems.
|
|
"""
|
|
hocr_path = resources / "hello_world_scripts.hocr"
|
|
if not hocr_path.exists():
|
|
pytest.skip("hello_world_scripts.hocr not found")
|
|
|
|
parser = HocrParser(hocr_path)
|
|
page = parser.parse()
|
|
|
|
# Build pages_data list as expected by MultiPageRenderer
|
|
pages_data = [(1, page, 300)] # (page_number, page_element, dpi)
|
|
|
|
renderer = Fpdf2MultiPageRenderer(
|
|
pages_data=pages_data,
|
|
multi_font_manager=multi_font_manager,
|
|
invisible_text=False,
|
|
)
|
|
|
|
output_path = tmp_path / "hello_world_scripts_multipage.pdf"
|
|
renderer.render(output_path)
|
|
|
|
assert output_path.exists()
|
|
assert output_path.stat().st_size > 0
|