From 23f42e13b1948f103eb26df10d7579b5c9d82099 Mon Sep 17 00:00:00 2001 From: Ryan King Date: Wed, 2 Sep 2026 12:02:53 -0400 Subject: [PATCH] fix: return 0 instead of None when calculate_page_offset has no valid pairs Fixes a crash in standard mode when processing PDFs that have no matching page pairs between the table of contents page numbers and physical indices. When calculate_page_offset() cannot determine an offset (no valid pairs), it now returns 0 instead of None, allowing add_page_offset_to_toc_json() to safely perform arithmetic operations. --- pageindex/page_index_classic.py | 10 ++-- tests/test_calculate_page_offset.py | 86 +++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 tests/test_calculate_page_offset.py diff --git a/pageindex/page_index_classic.py b/pageindex/page_index_classic.py index 446e8893b..8d03e8c00 100644 --- a/pageindex/page_index_classic.py +++ b/pageindex/page_index_classic.py @@ -490,16 +490,16 @@ def calculate_page_offset(pairs): differences.append(difference) except (KeyError, TypeError): continue - + if not differences: - return None - + return 0 + difference_counts = {} for diff in differences: difference_counts[diff] = difference_counts.get(diff, 0) + 1 - + most_common = max(difference_counts.items(), key=lambda x: x[1])[0] - + return most_common def add_page_offset_to_toc_json(data, offset): diff --git a/tests/test_calculate_page_offset.py b/tests/test_calculate_page_offset.py new file mode 100644 index 000000000..d8dcafb17 --- /dev/null +++ b/tests/test_calculate_page_offset.py @@ -0,0 +1,86 @@ +import unittest + +from pageindex.page_index_classic import calculate_page_offset + + +class CalculatePageOffsetTest(unittest.TestCase): + """Tests for calculate_page_offset function""" + + def test_calculate_offset_with_valid_pairs(self): + """Should calculate the most common offset from valid page pairs""" + pairs = [ + {'title': 'Section 1', 'page': 5, 'physical_index': 10}, # offset = 5 + {'title': 'Section 2', 'page': 10, 'physical_index': 15}, # offset = 5 + {'title': 'Section 3', 'page': 15, 'physical_index': 20}, # offset = 5 + ] + + result = calculate_page_offset(pairs) + + self.assertEqual(result, 5) + + def test_calculate_offset_returns_most_common(self): + """Should return the most common offset when there are variations""" + pairs = [ + {'title': 'Section 1', 'page': 5, 'physical_index': 10}, # offset = 5 + {'title': 'Section 2', 'page': 10, 'physical_index': 15}, # offset = 5 + {'title': 'Section 3', 'page': 15, 'physical_index': 20}, # offset = 5 + {'title': 'Section 4', 'page': 20, 'physical_index': 24}, # offset = 4 (outlier) + ] + + result = calculate_page_offset(pairs) + + self.assertEqual(result, 5) # Most common offset wins + + def test_calculate_offset_with_empty_pairs(self): + """Should return 0 when given empty pairs list""" + pairs = [] + + result = calculate_page_offset(pairs) + + self.assertEqual(result, 0) + + def test_calculate_offset_with_invalid_pairs(self): + """Should return 0 when all pairs have missing or invalid data""" + pairs = [ + {'title': 'Section 1'}, # Missing page and physical_index + {'title': 'Section 2', 'page': None, 'physical_index': 10}, + {'title': 'Section 3', 'page': 5, 'physical_index': None}, + {'title': 'Section 4', 'page': 'invalid', 'physical_index': 10}, + ] + + result = calculate_page_offset(pairs) + + self.assertEqual(result, 0) + + def test_calculate_offset_with_mixed_valid_invalid(self): + """Should calculate offset from valid pairs, ignoring invalid ones""" + pairs = [ + {'title': 'Section 1', 'page': 5, 'physical_index': 10}, # offset = 5 + {'title': 'Section 2'}, # Invalid - missing data + {'title': 'Section 3', 'page': 10, 'physical_index': 15}, # offset = 5 + {'title': 'Section 4', 'page': None, 'physical_index': 20}, # Invalid + ] + + result = calculate_page_offset(pairs) + + self.assertEqual(result, 5) + + def test_calculate_offset_always_returns_int(self): + """Should always return an integer, never None""" + test_cases = [ + [], # Empty list + [{'title': 'Section'}], # Missing data + [{'page': None, 'physical_index': None}], # Null values + ] + + for pairs in test_cases: + with self.subTest(pairs=pairs): + result = calculate_page_offset(pairs) + + self.assertIsNotNone(result, "calculate_page_offset should never return None") + self.assertIsInstance(result, int, "calculate_page_offset should always return int") + self.assertEqual(result, 0, "Should return 0 when offset cannot be calculated") + + +if __name__ == "__main__": + unittest.main()