From f4692cdd92c430e56ab9ff7698d04b887508193e Mon Sep 17 00:00:00 2001 From: Rossi-Luciano Date: Mon, 31 Aug 2026 07:05:53 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20migra=20PAGE=5FATTRIBUTES=20hardcoded=20?= =?UTF-8?q?para=20JSON=20de=20configura=C3=A7=C3=A3o=20(Fase=201,=20#1278)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packtools/sps/formats/pdf/default_layout.json | 19 +++++ packtools/sps/formats/pdf/enum.py | 30 +++---- packtools/sps/formats/pdf/layout_config.py | 59 ++++++++++++++ tests/sps/formats/pdf/test_layout_config.py | 80 +++++++++++++++++++ 4 files changed, 168 insertions(+), 20 deletions(-) create mode 100644 packtools/sps/formats/pdf/default_layout.json create mode 100644 packtools/sps/formats/pdf/layout_config.py create mode 100644 tests/sps/formats/pdf/test_layout_config.py diff --git a/packtools/sps/formats/pdf/default_layout.json b/packtools/sps/formats/pdf/default_layout.json new file mode 100644 index 000000000..e28a5c170 --- /dev/null +++ b/packtools/sps/formats/pdf/default_layout.json @@ -0,0 +1,19 @@ +{ + "schema_version": 1, + "page": { + "page_width_pt": 595.275591, + "page_height_pt": 841.889764, + "orientation": "portrait", + "top_margin_pt": 99.212598, + "left_margin_pt": 56.692913, + "right_margin_pt": 56.692913, + "bottom_margin_pt": 56.692913, + "header_distance_pt": 28.346457, + "footer_distance_pt": 28.346457, + "gutter_pt": 0, + "different_first_page_header_footer": true, + "body_start_type": "continuous", + "default_column_count": 2, + "column_spacing_pt": 15.0 + } +} diff --git a/packtools/sps/formats/pdf/enum.py b/packtools/sps/formats/pdf/enum.py index b6daa7c9e..0c40bdbaa 100644 --- a/packtools/sps/formats/pdf/enum.py +++ b/packtools/sps/formats/pdf/enum.py @@ -1,25 +1,19 @@ from docx.enum.style import WD_STYLE_TYPE from docx.enum.section import WD_ORIENT, WD_SECTION -from docx.shared import Cm + +from packtools.sps.formats.pdf import layout_config NAMESPACES = {'xml': 'http://www.w3.org/XML/1998/namespace'} -PAGE_ATTRIBUTES = { - "top_margin": Cm(3.5), - "left_margin": Cm(2), - "right_margin": Cm(2), - "bottom_margin": Cm(2), - "header_distance": Cm(1), - "footer_distance": Cm(1), - "gutter": Cm(0), - "orientation": WD_ORIENT.PORTRAIT, - "page_width": Cm(21.0), - "page_height": Cm(29.7), - "different_first_page_header_footer": True, - "start_type": WD_SECTION.CONTINUOUS, - "default_column_count": 2, -} +# Page/layout attributes and column spacing come from default_layout.json +# (packaged alongside this module) instead of being hardcoded here, so a +# journal-specific layout can eventually override them by pointing the +# loader at a different file. +PAGE_ATTRIBUTES = layout_config.load_page_attributes() + +# TWO_COLUMNS_SPACING is the space between two columns in twocolumn layout, measured in twips (1/20 of a point). +TWO_COLUMNS_SPACING = layout_config.load_column_spacing_twips() SUPPORTED_STYLES = [ WD_STYLE_TYPE.CHARACTER, @@ -27,9 +21,5 @@ WD_STYLE_TYPE.TABLE, ] -# TWO_COLUMNS_SPACING is the space between two columns in twocolumn layout, measured in twips (1/20 of a point). -# 300 twips = 15 points = ~5.29 mm -TWO_COLUMNS_SPACING = 300 - SINGLE_COLUMN_PAGE_LABEL = 'single-column-layout' DOUBLE_COLUMN_PAGE_LABEL = 'double-column-layout' diff --git a/packtools/sps/formats/pdf/layout_config.py b/packtools/sps/formats/pdf/layout_config.py new file mode 100644 index 000000000..6164304ed --- /dev/null +++ b/packtools/sps/formats/pdf/layout_config.py @@ -0,0 +1,59 @@ +import json +from pathlib import Path + +from docx.enum.section import WD_ORIENT, WD_SECTION +from docx.shared import Pt + +_DEFAULT_PATH = Path(__file__).parent / "default_layout.json" + +_ORIENTATION_MAP = { + "portrait": WD_ORIENT.PORTRAIT, + "landscape": WD_ORIENT.LANDSCAPE, +} + +_START_TYPE_MAP = { + "continuous": WD_SECTION.CONTINUOUS, + "new_page": WD_SECTION.NEW_PAGE, + "even_page": WD_SECTION.EVEN_PAGE, + "odd_page": WD_SECTION.ODD_PAGE, +} + + +def load_page_attributes(path=None): + """ + Load page/layout attributes from a layout-config JSON, returning a dict + shaped like the legacy PAGE_ATTRIBUTES (Pt/Cm Length objects and + python-docx enums), for drop-in use wherever PAGE_ATTRIBUTES is passed + today. + + `path` lets a caller point at a different file; defaults to the + packaged default_layout.json. Not wired to the CLI/API yet - that + comes with per-journal config support. + """ + data = json.loads(Path(path or _DEFAULT_PATH).read_text()) + page = data["page"] + + return { + "top_margin": Pt(page["top_margin_pt"]), + "left_margin": Pt(page["left_margin_pt"]), + "right_margin": Pt(page["right_margin_pt"]), + "bottom_margin": Pt(page["bottom_margin_pt"]), + "header_distance": Pt(page["header_distance_pt"]), + "footer_distance": Pt(page["footer_distance_pt"]), + "gutter": Pt(page["gutter_pt"]), + "orientation": _ORIENTATION_MAP[page["orientation"]], + "page_width": Pt(page["page_width_pt"]), + "page_height": Pt(page["page_height_pt"]), + "different_first_page_header_footer": page["different_first_page_header_footer"], + "start_type": _START_TYPE_MAP[page["body_start_type"]], + "default_column_count": page["default_column_count"], + } + + +def load_column_spacing_twips(path=None): + """ + Load the column spacing (in twips, 1/20 of a point) from the same + layout-config JSON used by load_page_attributes. + """ + data = json.loads(Path(path or _DEFAULT_PATH).read_text()) + return round(data["page"]["column_spacing_pt"] * 20) diff --git a/tests/sps/formats/pdf/test_layout_config.py b/tests/sps/formats/pdf/test_layout_config.py new file mode 100644 index 000000000..577f96036 --- /dev/null +++ b/tests/sps/formats/pdf/test_layout_config.py @@ -0,0 +1,80 @@ +import json +import tempfile +import unittest +from pathlib import Path + +from docx.enum.section import WD_ORIENT, WD_SECTION + +from packtools.sps.formats.pdf import layout_config + + +class TestLoadPageAttributes(unittest.TestCase): + """ + Regression tests for the JSON-backed replacement of the legacy hardcoded + PAGE_ATTRIBUTES dict. Length values are compared with a tolerance of a + few EMU: converting through a decimal pt value in JSON can't guarantee + perfect round-trip precision from the original Cm() literals, and a + handful of EMU (1/914400 inch) has no visible or functional effect. + """ + + def setUp(self): + self.attrs = layout_config.load_page_attributes() + + def _assert_close(self, actual, expected, tolerance_emu=5): + self.assertLessEqual(abs(int(actual) - int(expected)), tolerance_emu) + + def test_matches_legacy_hardcoded_values(self): + from docx.shared import Cm + + self._assert_close(self.attrs['top_margin'], Cm(3.5)) + self._assert_close(self.attrs['left_margin'], Cm(2)) + self._assert_close(self.attrs['right_margin'], Cm(2)) + self._assert_close(self.attrs['bottom_margin'], Cm(2)) + self._assert_close(self.attrs['header_distance'], Cm(1)) + self._assert_close(self.attrs['footer_distance'], Cm(1)) + self._assert_close(self.attrs['gutter'], Cm(0)) + self._assert_close(self.attrs['page_width'], Cm(21.0)) + self._assert_close(self.attrs['page_height'], Cm(29.7)) + self.assertEqual(self.attrs['orientation'], WD_ORIENT.PORTRAIT) + self.assertEqual(self.attrs['different_first_page_header_footer'], True) + self.assertEqual(self.attrs['start_type'], WD_SECTION.CONTINUOUS) + self.assertEqual(self.attrs['default_column_count'], 2) + + def test_load_column_spacing_twips_matches_legacy_constant(self): + self.assertEqual(layout_config.load_column_spacing_twips(), 300) + + def test_custom_path_overrides_default(self): + custom = { + "schema_version": 1, + "page": { + "page_width_pt": 400.0, + "page_height_pt": 600.0, + "orientation": "landscape", + "top_margin_pt": 10.0, + "left_margin_pt": 10.0, + "right_margin_pt": 10.0, + "bottom_margin_pt": 10.0, + "header_distance_pt": 5.0, + "footer_distance_pt": 5.0, + "gutter_pt": 0, + "different_first_page_header_footer": False, + "body_start_type": "new_page", + "default_column_count": 1, + "column_spacing_pt": 0, + }, + } + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "custom.json" + path.write_text(json.dumps(custom)) + + attrs = layout_config.load_page_attributes(path) + self.assertEqual(attrs['orientation'], WD_ORIENT.LANDSCAPE) + self.assertEqual(attrs['start_type'], WD_SECTION.NEW_PAGE) + self.assertEqual(attrs['default_column_count'], 1) + self.assertEqual(attrs['different_first_page_header_footer'], False) + + self.assertEqual(layout_config.load_column_spacing_twips(path), 0) + + +if __name__ == "__main__": + unittest.main()