# Copyright (c) 2026 PaddlePaddle Authors. All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """Tests for the layout parsing patches (issue #17503). These tests verify the fixed functions directly without importing the full paddleocr package (which requires paddlex). """ from pathlib import Path import importlib.util import numpy as np import pytest REPO_ROOT = Path(__file__).resolve().parents[2] # Import the patch module directly to avoid triggering the full paddleocr # import chain which requires paddlex def _import_patch_module(): """Import _patch_layout_parsing without triggering paddleocr.__init__.""" spec = importlib.util.spec_from_file_location( "paddleocr._pipelines._patch_layout_parsing", REPO_ROOT / "paddleocr" / "_pipelines" / "_patch_layout_parsing.py", ) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) return mod _patch_mod = _import_patch_module() _fixed_calculate_overlap_ratio = _patch_mod._fixed_calculate_overlap_ratio _fixed_calculate_minimum_enclosing_bbox = ( _patch_mod._fixed_calculate_minimum_enclosing_bbox ) class TestFixedCalculateOverlapRatio: """Tests for the overflow-safe calculate_overlap_ratio.""" def test_normal_overlap(self): bbox1 = [0, 0, 100, 100] bbox2 = [50, 50, 150, 150] ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="union") # Intersection: 50x50 = 2500 # Union: 10000 + 10000 - 2500 = 17500 expected = 2500.0 / 17500.0 assert abs(ratio - expected) < 1e-6 def test_no_overlap(self): bbox1 = [0, 0, 50, 50] bbox2 = [100, 100, 200, 200] ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="union") assert ratio == 0.0 def test_complete_overlap(self): bbox1 = [0, 0, 100, 100] bbox2 = [0, 0, 100, 100] ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="union") assert abs(ratio - 1.0) < 1e-6 def test_large_coordinates_no_overflow(self): """Verify no integer overflow with large coordinate values. This is the primary bug from issue #17503. With int32 arithmetic, inter_width * inter_height would overflow for large images (e.g. after document unwarping). """ # Coordinates large enough to cause int32 overflow when multiplied bbox1 = np.array([0, 0, 50000, 50000], dtype=np.int32) bbox2 = np.array([0, 0, 50000, 50000], dtype=np.int32) ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="union") # Should be 1.0 (identical boxes), not corrupted by overflow assert abs(ratio - 1.0) < 1e-6 def test_large_coordinates_partial_overlap(self): """Verify correct overlap ratio with large coordinates.""" bbox1 = np.array([0, 0, 60000, 60000], dtype=np.int32) bbox2 = np.array([30000, 30000, 90000, 90000], dtype=np.int32) ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="small") # Intersection: 30000x30000 = 900_000_000 # Small box area: 60000*60000 = 3_600_000_000 expected = 900_000_000.0 / 3_600_000_000.0 assert abs(ratio - expected) < 1e-6 def test_mode_small(self): bbox1 = [0, 0, 100, 100] bbox2 = [0, 0, 200, 200] ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="small") # Intersection = 100*100 = 10000, small area = 10000 assert abs(ratio - 1.0) < 1e-6 def test_mode_large(self): bbox1 = [0, 0, 100, 100] bbox2 = [0, 0, 200, 200] ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="large") # Intersection = 100*100 = 10000, large area = 40000 expected = 10000.0 / 40000.0 assert abs(ratio - expected) < 1e-6 def test_zero_area_bbox(self): bbox1 = [0, 0, 0, 0] bbox2 = [0, 0, 100, 100] ratio = _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="union") assert ratio == 0.0 def test_invalid_mode_raises(self): bbox1 = [0, 0, 100, 100] bbox2 = [50, 50, 150, 150] with pytest.raises(ValueError, match="Invalid mode"): _fixed_calculate_overlap_ratio(bbox1, bbox2, mode="invalid") class TestFixedCalculateMinimumEnclosingBbox: """Tests for the empty-safe calculate_minimum_enclosing_bbox.""" def test_single_bbox(self): result = _fixed_calculate_minimum_enclosing_bbox([[10, 20, 30, 40]]) np.testing.assert_array_equal(result, [10, 20, 30, 40]) def test_multiple_bboxes(self): bboxes = [[10, 20, 30, 40], [5, 15, 35, 45]] result = _fixed_calculate_minimum_enclosing_bbox(bboxes) np.testing.assert_array_equal(result, [5, 15, 35, 45]) def test_empty_list_returns_degenerate_bbox(self): """Verify empty list returns a degenerate bbox instead of raising. This is the secondary fix from issue #17503. The original code raises ValueError("The list of bounding boxes is empty."). """ result = _fixed_calculate_minimum_enclosing_bbox([]) assert result is not None np.testing.assert_array_equal(result, [0, 0, 0, 0]) def test_none_input_returns_degenerate_bbox(self): result = _fixed_calculate_minimum_enclosing_bbox(None) assert result is not None np.testing.assert_array_equal(result, [0, 0, 0, 0])