python-hwpx 3.4.1__tar.gz → 3.6.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_hwpx-3.4.1/src/python_hwpx.egg-info → python_hwpx-3.6.0}/PKG-INFO +2 -2
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/README.md +1 -1
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/pyproject.toml +1 -1
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/engine.py +268 -3
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/measure.py +207 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/opc/package.py +59 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/document_parts.py +8 -4
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/paragraph.py +3 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/run.py +4 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/table.py +11 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0/src/python_hwpx.egg-info}/PKG-INFO +2 -2
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/SOURCES.txt +1 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_document_formatting.py +9 -2
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_document_save_api.py +4 -1
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fit.py +187 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fit_integration.py +15 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_kordoc_absorption.py +8 -6
- python_hwpx-3.6.0/tests/test_layout_cache_scope.py +122 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_opc_package.py +38 -6
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/LICENSE +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/NOTICE +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/setup.cfg +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/dump.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/model.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/native.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/blueprint/replay.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/commands.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/form_plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/model.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/path.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/query.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/agent/story.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/authoring.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/benchmark/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/benchmark/blind_eval.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/builder/core.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/builder/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/badges.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/conformance/runner.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/_support.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/composer.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/harvest.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/design/validator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/evalplan_fill.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/exam/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/exam/compose.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/exam/ir.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/exam/measure.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/exam/parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/exam/profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/fill_residue.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/seal.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/form_fit/wordbox.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/formfill_quality.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/guidance_scan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/package.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/official_lint.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/pii.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/detectors.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/fixture_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/hancom_worker.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/masks.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/page_qa.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/qa_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/qa_metrics.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/hwpx/visual/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_blueprint_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_blueprint_dump.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_blueprint_replay.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_commands.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_mixed_form.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_path_query.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_agent_story_commands.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_authoring_native_toc.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_authoring_profile_routing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_authoring_render_check.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_body_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_conformance.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_corpus_toc_verify.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_design_builder.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_document_plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_evalplan_fill.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_compose.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_compose_oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_fixtures.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_ir.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_measure.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_exam_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_fill_residue.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fit_seal.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fit_seal_placement.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_form_fit_wordbox.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_formfill_differential_driver.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_formfill_quality.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_guidance_scan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_hancom_render_worker.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_ingest.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_m9_p0_spike_tools.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_mail_merge_fit.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_mixed_form_plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_new_features.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_official_lint_gongmun_gate.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_official_lint_tableaware.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_openrate.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_pii.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_pii_leak_sweep.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_pii_structural.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_pii_wiring.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_question_split_detector.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_rhwp_t1_gates.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_rhwp_t2_verification.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_save_pipeline_no_bypass.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_version_metadata.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_visual_fixture_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_visual_oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_visual_oracle_budget.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_visual_qa_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.6.0}/tests/test_visual_qa_metrics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.6.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -70,7 +70,7 @@ Dynamic: license-file
|
|
|
70
70
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
71
71
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
72
72
|
|
|
73
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
73
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
|
|
74
74
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
75
75
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
76
76
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
`hwpx-mcp-server`와 `hwpx-plugin`은 같은 프로젝트가 직접 유지보수하는 first-party 연동 구성요소입니다.
|
|
26
26
|
“first-party”는 프로젝트 유지보수 관계를 뜻하며, 한컴 또는 제3자의 공식 인증을 뜻하지 않습니다.
|
|
27
27
|
|
|
28
|
-
현재 PyPI 공개 릴리스는 `python-hwpx 3.
|
|
28
|
+
현재 PyPI 공개 릴리스는 `python-hwpx 3.6.0`입니다. 일반
|
|
29
29
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
30
30
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
31
31
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "3.
|
|
7
|
+
version = "3.6.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 자동화 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -17,13 +17,32 @@ from __future__ import annotations
|
|
|
17
17
|
|
|
18
18
|
from dataclasses import dataclass, replace
|
|
19
19
|
|
|
20
|
-
from .measure import
|
|
20
|
+
from .measure import (
|
|
21
|
+
GROSS_ROW_GROWTH_FACTOR,
|
|
22
|
+
MIN_ROW_GROWTH_LINES,
|
|
23
|
+
SlotMetrics,
|
|
24
|
+
estimate_text_width,
|
|
25
|
+
measure,
|
|
26
|
+
)
|
|
21
27
|
from .policy import FitPolicy
|
|
22
28
|
from .report import FitResult
|
|
23
29
|
|
|
24
30
|
# Font ladder step (points) used when shrinking.
|
|
25
31
|
_SHRINK_STEP_PT = 0.5
|
|
26
32
|
|
|
33
|
+
# The vertical room could not be measured (merged cell, no height, or an auto-grow
|
|
34
|
+
# floor); the fit is width-only and the oracle must confirm the vertical fit.
|
|
35
|
+
_HEIGHT_UNAVAILABLE_NOTE = (
|
|
36
|
+
"cell height is not a usable budget (merged/auto-grow); fit is width-only — "
|
|
37
|
+
"render oracle should confirm the vertical fit"
|
|
38
|
+
)
|
|
39
|
+
# The content needs more vertical room than the authored cell height, but not
|
|
40
|
+
# grossly — the row will grow modestly. Reported (never silent), deferred to oracle.
|
|
41
|
+
_ROW_GROWTH_NOTE = (
|
|
42
|
+
"value needs more lines than the cell height budgets; the row will grow — "
|
|
43
|
+
"render oracle should confirm no page shift"
|
|
44
|
+
)
|
|
45
|
+
|
|
27
46
|
|
|
28
47
|
@dataclass(slots=True)
|
|
29
48
|
class FitEngine:
|
|
@@ -36,6 +55,26 @@ class FitEngine:
|
|
|
36
55
|
policy: FitPolicy | None = None,
|
|
37
56
|
*,
|
|
38
57
|
field_id: str | None = None,
|
|
58
|
+
) -> FitResult:
|
|
59
|
+
result = self._fit_decide(value, slot, policy, field_id=field_id)
|
|
60
|
+
inline_count = int(getattr(slot, "inline_object_count", 0) or 0)
|
|
61
|
+
if inline_count and (value or "") != "":
|
|
62
|
+
inline_width = float(getattr(slot, "inline_object_width", 0.0) or 0.0)
|
|
63
|
+
result.warnings.append(
|
|
64
|
+
f"cell line shared with {inline_count} inline control(s) "
|
|
65
|
+
f"({inline_width:.0f} HWPUNIT deducted from the width budget); "
|
|
66
|
+
"control-sharing cells are unusual fill targets — verify with a "
|
|
67
|
+
"render gate or pick the intended value cell"
|
|
68
|
+
)
|
|
69
|
+
return result
|
|
70
|
+
|
|
71
|
+
def _fit_decide(
|
|
72
|
+
self,
|
|
73
|
+
value: str,
|
|
74
|
+
slot: SlotMetrics,
|
|
75
|
+
policy: FitPolicy | None = None,
|
|
76
|
+
*,
|
|
77
|
+
field_id: str | None = None,
|
|
39
78
|
) -> FitResult:
|
|
40
79
|
policy = policy or FitPolicy()
|
|
41
80
|
value = "" if value is None else str(value)
|
|
@@ -58,12 +97,35 @@ class FitEngine:
|
|
|
58
97
|
if policy.mode == "expand_row":
|
|
59
98
|
return self._expand_row(value, slot, policy, field_id)
|
|
60
99
|
|
|
100
|
+
# Inline objects can consume the whole line; with less than a glyph of
|
|
101
|
+
# width left there is nothing to reshape into — refuse with a message
|
|
102
|
+
# that names the cause instead of an absurd wrapped-line estimate.
|
|
103
|
+
if (
|
|
104
|
+
int(getattr(slot, "inline_object_count", 0) or 0)
|
|
105
|
+
and slot.available_width < slot.font_pt * 100.0 * 0.6
|
|
106
|
+
):
|
|
107
|
+
return FitResult(
|
|
108
|
+
ok=False,
|
|
109
|
+
value=value,
|
|
110
|
+
applied_value=value,
|
|
111
|
+
lines=0,
|
|
112
|
+
font_pt=slot.font_pt,
|
|
113
|
+
overflow_detected=True,
|
|
114
|
+
confidence="high",
|
|
115
|
+
errors=[
|
|
116
|
+
"FIELD_OVERFLOW: inline control(s) leave no usable width in "
|
|
117
|
+
"this cell; choose the intended value cell or allow row "
|
|
118
|
+
"expansion"
|
|
119
|
+
],
|
|
120
|
+
field_id=field_id,
|
|
121
|
+
)
|
|
122
|
+
|
|
61
123
|
# Reshaping ladder: wrap, shrink, or wrap_then_shrink, then fail_on_overflow.
|
|
62
124
|
wrap_lines = policy.effective_max_lines if policy.may_wrap else 1
|
|
63
125
|
wrap_slot = replace(slot, max_lines=wrap_lines)
|
|
64
126
|
measurement = measure(value, wrap_slot)
|
|
65
127
|
if measurement.fits:
|
|
66
|
-
|
|
128
|
+
result = FitResult(
|
|
67
129
|
ok=True,
|
|
68
130
|
value=value,
|
|
69
131
|
applied_value=value,
|
|
@@ -78,11 +140,15 @@ class FitEngine:
|
|
|
78
140
|
warnings=list(measurement.notes),
|
|
79
141
|
field_id=field_id,
|
|
80
142
|
)
|
|
143
|
+
return self._apply_height_budget(result, value, slot, policy, wrap_lines, field_id)
|
|
81
144
|
|
|
82
145
|
if policy.may_shrink:
|
|
83
146
|
shrunk = self._try_shrink(value, slot, policy, wrap_lines)
|
|
84
147
|
if shrunk is not None:
|
|
85
|
-
|
|
148
|
+
shrunk = replace(shrunk, field_id=field_id)
|
|
149
|
+
return self._apply_height_budget(
|
|
150
|
+
shrunk, value, slot, policy, wrap_lines, field_id
|
|
151
|
+
)
|
|
86
152
|
|
|
87
153
|
# Still overflowing → terminal overflow action (confidence-gated).
|
|
88
154
|
return self._overflow(value, wrap_slot, policy, field_id, measurement)
|
|
@@ -212,6 +278,205 @@ class FitEngine:
|
|
|
212
278
|
candidate = round(candidate - _SHRINK_STEP_PT, 4)
|
|
213
279
|
return best
|
|
214
280
|
|
|
281
|
+
# -- vertical (row-height) budget ------------------------------------- #
|
|
282
|
+
def _apply_height_budget(
|
|
283
|
+
self,
|
|
284
|
+
result: FitResult,
|
|
285
|
+
value: str,
|
|
286
|
+
slot: SlotMetrics,
|
|
287
|
+
policy: FitPolicy,
|
|
288
|
+
wrap_lines: int,
|
|
289
|
+
field_id: str | None,
|
|
290
|
+
) -> FitResult:
|
|
291
|
+
"""Overlay the authored row-height budget on a width-fitting *result*.
|
|
292
|
+
|
|
293
|
+
The width ladder decides horizontal fit; this decides whether the value
|
|
294
|
+
also sits inside the cell's *vertical* room, so a filled row does not
|
|
295
|
+
silently grow and shift the page (the dominant M9 form-fill defect).
|
|
296
|
+
|
|
297
|
+
Honesty contract (mirrors the width path, grounded in the S-085 P0/P1
|
|
298
|
+
measurement): the authored ``cellSz.height`` is an unreliable proxy in wild
|
|
299
|
+
forms (auto-grow, tiny floors, merges), so a *modest* vertical overflow is
|
|
300
|
+
reported and deferred to the render oracle; only a *gross* balloon shrinks
|
|
301
|
+
or fails closed. ``allow_row_expand`` / ``expand_row`` opt out entirely.
|
|
302
|
+
"""
|
|
303
|
+
|
|
304
|
+
if slot.available_height is None:
|
|
305
|
+
if slot.height_unavailable and result.ok:
|
|
306
|
+
result.warnings.append(_HEIGHT_UNAVAILABLE_NOTE)
|
|
307
|
+
return result
|
|
308
|
+
if policy.allow_row_expand or policy.mode == "expand_row":
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
font = result.font_pt if result.font_pt is not None else slot.font_pt
|
|
312
|
+
wrapped = result.lines if result.lines is not None else 1
|
|
313
|
+
verdict = self._height_verdict(slot, wrapped, font)
|
|
314
|
+
if verdict == "fits":
|
|
315
|
+
return result
|
|
316
|
+
if verdict == "modest":
|
|
317
|
+
# S-085 P1 calibration round 2 (differential-measured): the modest
|
|
318
|
+
# band is where pages actually shift, so try to shrink INTO the
|
|
319
|
+
# budget first; only defer to the oracle when no font >= min can
|
|
320
|
+
# land the value fully inside the authored height.
|
|
321
|
+
if policy.may_shrink:
|
|
322
|
+
shrunk = self._try_shrink_for_height(
|
|
323
|
+
value, slot, policy, wrap_lines, field_id, accept_modest=False
|
|
324
|
+
)
|
|
325
|
+
if shrunk is not None:
|
|
326
|
+
return shrunk
|
|
327
|
+
result.overflow_detected = True
|
|
328
|
+
result.warnings.append(_ROW_GROWTH_NOTE)
|
|
329
|
+
return result
|
|
330
|
+
|
|
331
|
+
# Gross vertical balloon: shrink (prefer a full fit, else the least
|
|
332
|
+
# growth), else fail-closed.
|
|
333
|
+
if policy.may_shrink:
|
|
334
|
+
shrunk = self._try_shrink_for_height(
|
|
335
|
+
value, slot, policy, wrap_lines, field_id, accept_modest=True
|
|
336
|
+
)
|
|
337
|
+
if shrunk is not None:
|
|
338
|
+
return shrunk
|
|
339
|
+
return self._overflow_height(value, slot, policy, field_id, wrapped, font)
|
|
340
|
+
|
|
341
|
+
def _height_verdict(self, slot: SlotMetrics, wrapped_lines: int, font_pt: float) -> str:
|
|
342
|
+
"""Classify vertical fit as ``fits`` / ``modest`` / ``gross``."""
|
|
343
|
+
|
|
344
|
+
budget = slot.height_lines(font_pt)
|
|
345
|
+
if budget is None or wrapped_lines <= budget:
|
|
346
|
+
return "fits"
|
|
347
|
+
optimistic = slot.height_lines_optimistic(font_pt) or budget
|
|
348
|
+
if (
|
|
349
|
+
wrapped_lines > optimistic * GROSS_ROW_GROWTH_FACTOR
|
|
350
|
+
and (wrapped_lines - optimistic) >= MIN_ROW_GROWTH_LINES
|
|
351
|
+
):
|
|
352
|
+
return "gross"
|
|
353
|
+
return "modest"
|
|
354
|
+
|
|
355
|
+
def _try_shrink_for_height(
|
|
356
|
+
self,
|
|
357
|
+
value: str,
|
|
358
|
+
slot: SlotMetrics,
|
|
359
|
+
policy: FitPolicy,
|
|
360
|
+
wrap_lines: int,
|
|
361
|
+
field_id: str | None,
|
|
362
|
+
*,
|
|
363
|
+
accept_modest: bool,
|
|
364
|
+
) -> FitResult | None:
|
|
365
|
+
"""Largest font (>= min) that lands the value INSIDE the height budget.
|
|
366
|
+
|
|
367
|
+
Walks the shrink ladder preferring a full vertical fit ("fits"). When
|
|
368
|
+
``accept_modest`` is true and no font achieves a full fit, the largest
|
|
369
|
+
candidate whose growth is merely *modest* (never gross) is returned as
|
|
370
|
+
the least-damage fallback, reported and deferred to the oracle.
|
|
371
|
+
"""
|
|
372
|
+
|
|
373
|
+
ceiling = policy.max_font_pt or slot.font_pt
|
|
374
|
+
candidate = min(slot.font_pt, ceiling)
|
|
375
|
+
modest_candidate: tuple[float, int] | None = None
|
|
376
|
+
while candidate >= policy.min_font_pt - 1e-9:
|
|
377
|
+
trial = replace(slot, font_pt=candidate, max_lines=wrap_lines)
|
|
378
|
+
m = measure(value, trial)
|
|
379
|
+
if m.fits and m.confidence == "high":
|
|
380
|
+
verdict = self._height_verdict(slot, m.lines, candidate)
|
|
381
|
+
if verdict == "fits":
|
|
382
|
+
return self._shrunk_result(
|
|
383
|
+
value, slot, candidate, m.lines, wrap_lines, field_id,
|
|
384
|
+
modest=False,
|
|
385
|
+
)
|
|
386
|
+
if verdict == "modest" and modest_candidate is None:
|
|
387
|
+
modest_candidate = (candidate, m.lines)
|
|
388
|
+
candidate = round(candidate - _SHRINK_STEP_PT, 4)
|
|
389
|
+
if accept_modest and modest_candidate is not None:
|
|
390
|
+
font_pt, lines = modest_candidate
|
|
391
|
+
return self._shrunk_result(
|
|
392
|
+
value, slot, font_pt, lines, wrap_lines, field_id, modest=True
|
|
393
|
+
)
|
|
394
|
+
return None
|
|
395
|
+
|
|
396
|
+
def _shrunk_result(
|
|
397
|
+
self,
|
|
398
|
+
value: str,
|
|
399
|
+
slot: SlotMetrics,
|
|
400
|
+
candidate: float,
|
|
401
|
+
lines: int,
|
|
402
|
+
wrap_lines: int,
|
|
403
|
+
field_id: str | None,
|
|
404
|
+
*,
|
|
405
|
+
modest: bool,
|
|
406
|
+
) -> FitResult:
|
|
407
|
+
changes: dict[str, object] = {}
|
|
408
|
+
warnings: list[str] = []
|
|
409
|
+
if candidate != slot.font_pt:
|
|
410
|
+
changes = {
|
|
411
|
+
"font_pt": round(candidate, 2),
|
|
412
|
+
"from_font_pt": round(slot.font_pt, 2),
|
|
413
|
+
}
|
|
414
|
+
warnings.append(
|
|
415
|
+
f"font shrunk {slot.font_pt:g}pt → {candidate:g}pt to fit the row height"
|
|
416
|
+
)
|
|
417
|
+
if wrap_lines > 1 and lines > 1:
|
|
418
|
+
changes["wrapped_lines"] = lines
|
|
419
|
+
if modest:
|
|
420
|
+
warnings.append(_ROW_GROWTH_NOTE)
|
|
421
|
+
return FitResult(
|
|
422
|
+
ok=True,
|
|
423
|
+
value=value,
|
|
424
|
+
applied_value=value,
|
|
425
|
+
applied_style_changes=changes,
|
|
426
|
+
lines=lines,
|
|
427
|
+
font_pt=round(candidate, 2),
|
|
428
|
+
confidence="high",
|
|
429
|
+
overflow_detected=modest,
|
|
430
|
+
warnings=warnings,
|
|
431
|
+
field_id=field_id,
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
def _overflow_height(
|
|
435
|
+
self,
|
|
436
|
+
value: str,
|
|
437
|
+
slot: SlotMetrics,
|
|
438
|
+
policy: FitPolicy,
|
|
439
|
+
field_id: str | None,
|
|
440
|
+
wrapped_lines: int,
|
|
441
|
+
font_pt: float,
|
|
442
|
+
) -> FitResult:
|
|
443
|
+
"""Terminal action for a gross vertical balloon that shrink cannot resolve."""
|
|
444
|
+
|
|
445
|
+
budget = slot.height_lines(font_pt)
|
|
446
|
+
action = policy.overflow
|
|
447
|
+
# Line-level truncation is not modelled; keep the value and warn (honest).
|
|
448
|
+
if action == "truncate":
|
|
449
|
+
action = "warn"
|
|
450
|
+
if action == "warn":
|
|
451
|
+
return FitResult(
|
|
452
|
+
ok=True,
|
|
453
|
+
value=value,
|
|
454
|
+
applied_value=value,
|
|
455
|
+
lines=wrapped_lines,
|
|
456
|
+
font_pt=font_pt,
|
|
457
|
+
overflow_detected=True,
|
|
458
|
+
confidence="high",
|
|
459
|
+
warnings=[
|
|
460
|
+
f"value needs ~{wrapped_lines} lines but the row height holds "
|
|
461
|
+
f"~{budget}; the row will balloon (overflow=warn)"
|
|
462
|
+
],
|
|
463
|
+
field_id=field_id,
|
|
464
|
+
)
|
|
465
|
+
return FitResult(
|
|
466
|
+
ok=False,
|
|
467
|
+
value=value,
|
|
468
|
+
applied_value=value,
|
|
469
|
+
lines=wrapped_lines,
|
|
470
|
+
font_pt=font_pt,
|
|
471
|
+
overflow_detected=True,
|
|
472
|
+
confidence="high",
|
|
473
|
+
errors=[
|
|
474
|
+
f"FIELD_OVERFLOW: value needs ~{wrapped_lines} lines but the row height "
|
|
475
|
+
f"holds ~{budget}; shrink/shorten the value or allow row expansion"
|
|
476
|
+
],
|
|
477
|
+
field_id=field_id,
|
|
478
|
+
)
|
|
479
|
+
|
|
215
480
|
# -- terminal overflow action ----------------------------------------- #
|
|
216
481
|
def _overflow(
|
|
217
482
|
self,
|
|
@@ -53,6 +53,31 @@ _CLASS_UNCERTAINTY: dict[str, float] = {
|
|
|
53
53
|
# multiplicative factor so it scales and also buys headroom on the advance error.
|
|
54
54
|
DEFAULT_SAFETY = 0.93
|
|
55
55
|
|
|
56
|
+
# --- Vertical (line-height) model ------------------------------------------- #
|
|
57
|
+
# Per-line vertical advance as a multiple of the em (font height in HWPUNIT).
|
|
58
|
+
# HWP's application default is 160% line spacing, so a line occupies ~1.6 em; this
|
|
59
|
+
# is the advance used when a paragraph declares no explicit lineSpacing, and it
|
|
60
|
+
# matches ``layout.lint._LINE_SPACING``. Used as the *expected* pitch for the
|
|
61
|
+
# authored vertical budget (the point at which the row is warned it will grow).
|
|
62
|
+
DEFAULT_LINE_SPACING_RATIO = 1.6
|
|
63
|
+
|
|
64
|
+
# Tightest per-line advance Hancom is observed to use (a glyph box with no leading).
|
|
65
|
+
# Measured on the M9 wild-form corpus: two soft-wrapped lines render inside cells
|
|
66
|
+
# only ~2.0 em tall, so the real pitch bottoms out near 1.0 em. Used to compute the
|
|
67
|
+
# *optimistic* (most generous) height budget, so a vertical overflow is only treated
|
|
68
|
+
# as high confidence when content overflows even at this tightest pitch.
|
|
69
|
+
MIN_LINE_SPACING_RATIO = 1.0
|
|
70
|
+
|
|
71
|
+
# A vertical overflow is a *hard* balloon (shrink-or-refuse) only when content needs
|
|
72
|
+
# at least this multiple of the optimistic authored line budget AND this many extra
|
|
73
|
+
# lines in absolute terms. Below the gross threshold the row may grow only modestly
|
|
74
|
+
# and the render oracle arbitrates. Measure-first calibration on the M9 corpus: a
|
|
75
|
+
# hard cap at the optimistic budget mis-refuses 47% of known-good multi-line cells,
|
|
76
|
+
# whereas this gross gate mis-refuses 2% — it fires on page-shifting balloons only.
|
|
77
|
+
# (Same balloon philosophy as ``layout.lint._BALLOON_FACTOR``.)
|
|
78
|
+
GROSS_ROW_GROWTH_FACTOR = 2.0
|
|
79
|
+
MIN_ROW_GROWTH_LINES = 2
|
|
80
|
+
|
|
56
81
|
Confidence = Literal["high", "low"]
|
|
57
82
|
|
|
58
83
|
|
|
@@ -189,11 +214,69 @@ class SlotMetrics:
|
|
|
189
214
|
max_lines: int = 1
|
|
190
215
|
raw_width: float | None = None # cellSz.width before margins (diagnostics)
|
|
191
216
|
source: str = "cell"
|
|
217
|
+
# Vertical budget. ``available_height`` is the usable inner height (HWPUNIT)
|
|
218
|
+
# after top/bottom cell margins + the safety inset. ``None`` means the vertical
|
|
219
|
+
# room was not (or could not be) measured, and the fit stays width-only.
|
|
220
|
+
available_height: float | None = None
|
|
221
|
+
# Per-line advance as a multiple of the em, from the cell's declared paragraph
|
|
222
|
+
# line spacing (PERCENT). ``None`` falls back to ``DEFAULT_LINE_SPACING_RATIO``.
|
|
223
|
+
line_spacing_ratio: float | None = None
|
|
224
|
+
# Width already consumed on the line by inline treat-as-char objects
|
|
225
|
+
# (checkboxes, form controls, pictures) that share the target paragraph.
|
|
226
|
+
# Their declared ``hp:sz/@width`` is subtracted from the usable width —
|
|
227
|
+
# ignoring them made the engine call "fits" on a fill that real Hancom
|
|
228
|
+
# wrapped, growing the row and repaginating a 10-page form (S-087 P0).
|
|
229
|
+
inline_object_width: float = 0.0
|
|
230
|
+
inline_object_count: int = 0
|
|
231
|
+
# A cell height existed but was unusable (merged row-span fragment, or an
|
|
232
|
+
# auto-grow floor shorter than one line). Records "height budget unavailable"
|
|
233
|
+
# so the fit reports width-only honestly rather than guessing a vertical fit.
|
|
234
|
+
height_unavailable: bool = False
|
|
192
235
|
|
|
193
236
|
@property
|
|
194
237
|
def capacity(self) -> float:
|
|
195
238
|
return self.available_width * self.max_lines
|
|
196
239
|
|
|
240
|
+
def _line_ratio(self) -> float:
|
|
241
|
+
ratio = self.line_spacing_ratio
|
|
242
|
+
return ratio if ratio and ratio > 0 else DEFAULT_LINE_SPACING_RATIO
|
|
243
|
+
|
|
244
|
+
def line_height(self, font_pt: float | None = None) -> float:
|
|
245
|
+
"""Expected per-line vertical advance in HWPUNIT at *font_pt*."""
|
|
246
|
+
|
|
247
|
+
pt = self.font_pt if font_pt is None else font_pt
|
|
248
|
+
return pt * 100.0 * self._line_ratio()
|
|
249
|
+
|
|
250
|
+
def height_lines(self, font_pt: float | None = None) -> int | None:
|
|
251
|
+
"""Expected vertical line budget at *font_pt* (declared/default pitch).
|
|
252
|
+
|
|
253
|
+
``None`` when the vertical room is unmeasured. Never less than 1 — a cell
|
|
254
|
+
always accommodates its first line; the risk we guard is *growth* past it.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
if self.available_height is None:
|
|
258
|
+
return None
|
|
259
|
+
line_h = self.line_height(font_pt)
|
|
260
|
+
if line_h <= 0:
|
|
261
|
+
return None
|
|
262
|
+
return max(int(self.available_height // line_h), 1)
|
|
263
|
+
|
|
264
|
+
def height_lines_optimistic(self, font_pt: float | None = None) -> int | None:
|
|
265
|
+
"""Most-generous vertical budget (tightest plausible pitch).
|
|
266
|
+
|
|
267
|
+
This is the basis for the *confidence* of a vertical overflow: content that
|
|
268
|
+
overflows even this budget is grossly too tall regardless of pitch error.
|
|
269
|
+
"""
|
|
270
|
+
|
|
271
|
+
if self.available_height is None:
|
|
272
|
+
return None
|
|
273
|
+
pt = self.font_pt if font_pt is None else font_pt
|
|
274
|
+
ratio = min(self._line_ratio(), MIN_LINE_SPACING_RATIO)
|
|
275
|
+
line_h = pt * 100.0 * ratio
|
|
276
|
+
if line_h <= 0:
|
|
277
|
+
return None
|
|
278
|
+
return max(int(self.available_height // line_h), 1)
|
|
279
|
+
|
|
197
280
|
|
|
198
281
|
@dataclass(slots=True)
|
|
199
282
|
class Measurement:
|
|
@@ -291,6 +374,50 @@ def _cell_margin(cell_element: object) -> tuple[int, int]:
|
|
|
291
374
|
return (0, 0)
|
|
292
375
|
|
|
293
376
|
|
|
377
|
+
def _cell_margin_vertical(cell_element: object) -> tuple[int, int]:
|
|
378
|
+
"""Return (top, bottom) cellMargin in HWPUNIT, defaulting to 0."""
|
|
379
|
+
|
|
380
|
+
for child in cell_element: # type: ignore[attr-defined]
|
|
381
|
+
if _local_name(child.tag) == "cellMargin":
|
|
382
|
+
return (
|
|
383
|
+
int(child.get("top", "0") or 0),
|
|
384
|
+
int(child.get("bottom", "0") or 0),
|
|
385
|
+
)
|
|
386
|
+
return (0, 0)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
def _first_para_line_spacing_ratio(cell: object, document: object) -> float | None:
|
|
390
|
+
"""Per-line em multiple from the cell's first paragraph line spacing (PERCENT).
|
|
391
|
+
|
|
392
|
+
Only PERCENT spacing maps cleanly onto the em-relative line-height model; FIXED
|
|
393
|
+
/ ATLEAST / BETWEENLINES are left to the conservative default so we never invent
|
|
394
|
+
a font-independent pitch (the shrink ladder varies the font).
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
paragraphs = cell.paragraphs # type: ignore[attr-defined]
|
|
399
|
+
except Exception: # pragma: no cover - defensive
|
|
400
|
+
return None
|
|
401
|
+
for paragraph in paragraphs:
|
|
402
|
+
ref = getattr(paragraph, "para_pr_id_ref", None)
|
|
403
|
+
if ref is None or document is None:
|
|
404
|
+
continue
|
|
405
|
+
try:
|
|
406
|
+
prop = document.paragraph_property(ref) # type: ignore[attr-defined]
|
|
407
|
+
except Exception: # pragma: no cover - defensive
|
|
408
|
+
prop = None
|
|
409
|
+
spacing = getattr(prop, "line_spacing", None) if prop is not None else None
|
|
410
|
+
if spacing is None or not getattr(spacing, "value", None):
|
|
411
|
+
continue
|
|
412
|
+
if (getattr(spacing, "spacing_type", None) or "PERCENT").upper() != "PERCENT":
|
|
413
|
+
return None
|
|
414
|
+
try:
|
|
415
|
+
return int(spacing.value) / 100.0
|
|
416
|
+
except (TypeError, ValueError): # pragma: no cover - defensive
|
|
417
|
+
return None
|
|
418
|
+
return None
|
|
419
|
+
|
|
420
|
+
|
|
294
421
|
def _first_run_font_pt(cell: object, document: object) -> float:
|
|
295
422
|
"""Resolve the cell's first run font size in points (default 10pt)."""
|
|
296
423
|
|
|
@@ -339,27 +466,107 @@ def resolve_slot_metrics(
|
|
|
339
466
|
verified against Hancom's own ``lineSeg/@horzsize`` (±10 HWPUNIT on 82% of
|
|
340
467
|
cells; the safety factor covers the rest plus paragraph indent, which is left
|
|
341
468
|
to the HarfBuzz pass).
|
|
469
|
+
|
|
470
|
+
``available_height`` follows the same philosophy — ``(cellSz.height - top -
|
|
471
|
+
bottom margin) * safety`` — but is recorded as *unavailable* (``None`` +
|
|
472
|
+
``height_unavailable``) whenever the authored height is not a real ceiling:
|
|
473
|
+
a merged cell (its ``cellSz.height`` is a single-row fragment, not the spanned
|
|
474
|
+
height), a cell with no height, or an auto-grow floor shorter than one line
|
|
475
|
+
(Hancom simply grows the row past it). The fit then stays width-only rather
|
|
476
|
+
than guess a vertical fit.
|
|
342
477
|
"""
|
|
343
478
|
|
|
344
479
|
raw_width = float(getattr(cell, "width", 0) or 0)
|
|
345
480
|
element = getattr(cell, "element", None)
|
|
346
481
|
left, right = _cell_margin(element) if element is not None else (0, 0)
|
|
347
482
|
inner = max(raw_width - left - right, 0.0) * safety
|
|
483
|
+
inline_width, inline_count = (
|
|
484
|
+
_inline_object_width(element) if element is not None else (0.0, 0)
|
|
485
|
+
)
|
|
486
|
+
if inline_width:
|
|
487
|
+
inner = max(inner - inline_width, 0.0)
|
|
348
488
|
resolved_pt = font_pt if font_pt is not None else _first_run_font_pt(cell, document)
|
|
489
|
+
line_ratio = _first_para_line_spacing_ratio(cell, document)
|
|
490
|
+
|
|
491
|
+
available_height: float | None = None
|
|
492
|
+
height_unavailable = False
|
|
493
|
+
raw_height = float(getattr(cell, "height", 0) or 0)
|
|
494
|
+
try:
|
|
495
|
+
row_span = int(getattr(cell, "span", (1, 1))[0])
|
|
496
|
+
except Exception: # pragma: no cover - defensive
|
|
497
|
+
row_span = 1
|
|
498
|
+
top, bottom = _cell_margin_vertical(element) if element is not None else (0, 0)
|
|
499
|
+
inner_h = max(raw_height - top - bottom, 0.0) * safety if raw_height > 0 else 0.0
|
|
500
|
+
# A cell authored shorter than one line at the tightest pitch is an auto-grow
|
|
501
|
+
# floor, not a ceiling (Hancom grows the row past it); a merged row-span's
|
|
502
|
+
# cellSz.height is only one of the spanned rows. Neither is a usable budget.
|
|
503
|
+
one_line = resolved_pt * 100.0 * MIN_LINE_SPACING_RATIO
|
|
504
|
+
if row_span <= 1 and inner_h >= one_line:
|
|
505
|
+
available_height = inner_h
|
|
506
|
+
else:
|
|
507
|
+
height_unavailable = True
|
|
508
|
+
|
|
349
509
|
return SlotMetrics(
|
|
350
510
|
available_width=inner,
|
|
351
511
|
font_pt=resolved_pt,
|
|
352
512
|
max_lines=max(max_lines, 1),
|
|
353
513
|
raw_width=raw_width,
|
|
354
514
|
source="cell",
|
|
515
|
+
available_height=available_height,
|
|
516
|
+
line_spacing_ratio=line_ratio,
|
|
517
|
+
height_unavailable=height_unavailable,
|
|
518
|
+
inline_object_width=inline_width,
|
|
519
|
+
inline_object_count=inline_count,
|
|
355
520
|
)
|
|
356
521
|
|
|
357
522
|
|
|
523
|
+
def _inline_object_width(cell_element: object) -> tuple[float, int]:
|
|
524
|
+
"""Total declared width of inline treat-as-char objects in the cell.
|
|
525
|
+
|
|
526
|
+
Checkboxes and similar form controls flow on the text line
|
|
527
|
+
(``hp:pos/@treatAsChar='1'``) and consume their ``hp:sz/@width``; a fill
|
|
528
|
+
value shares whatever width remains. Objects without a declared size are
|
|
529
|
+
counted but contribute 0 width (the count still signals reduced trust).
|
|
530
|
+
"""
|
|
531
|
+
|
|
532
|
+
total = 0.0
|
|
533
|
+
count = 0
|
|
534
|
+
iter_fn = getattr(cell_element, "iter", None)
|
|
535
|
+
if iter_fn is None:
|
|
536
|
+
return 0.0, 0
|
|
537
|
+
for node in iter_fn():
|
|
538
|
+
tag = getattr(node, "tag", "")
|
|
539
|
+
if not isinstance(tag, str):
|
|
540
|
+
continue
|
|
541
|
+
local = tag.rsplit("}", 1)[-1].lower()
|
|
542
|
+
if local != "pos":
|
|
543
|
+
continue
|
|
544
|
+
if (node.get("treatAsChar") or "").strip() not in {"1", "true", "TRUE"}:
|
|
545
|
+
continue
|
|
546
|
+
holder = node.getparent() if hasattr(node, "getparent") else None
|
|
547
|
+
if holder is None:
|
|
548
|
+
continue
|
|
549
|
+
count += 1
|
|
550
|
+
for sibling in holder:
|
|
551
|
+
sib_local = str(getattr(sibling, "tag", "")).rsplit("}", 1)[-1].lower()
|
|
552
|
+
if sib_local == "sz":
|
|
553
|
+
try:
|
|
554
|
+
total += float(sibling.get("width") or 0)
|
|
555
|
+
except (TypeError, ValueError):
|
|
556
|
+
pass
|
|
557
|
+
break
|
|
558
|
+
return total, count
|
|
559
|
+
|
|
560
|
+
|
|
358
561
|
__all__ = [
|
|
359
562
|
"SlotMetrics",
|
|
360
563
|
"Measurement",
|
|
361
564
|
"Confidence",
|
|
362
565
|
"DEFAULT_SAFETY",
|
|
566
|
+
"DEFAULT_LINE_SPACING_RATIO",
|
|
567
|
+
"MIN_LINE_SPACING_RATIO",
|
|
568
|
+
"GROSS_ROW_GROWTH_FACTOR",
|
|
569
|
+
"MIN_ROW_GROWTH_LINES",
|
|
363
570
|
"classify_char",
|
|
364
571
|
"char_advance",
|
|
365
572
|
"estimate_text_width",
|
|
@@ -159,10 +159,69 @@ def _local_name(element: etree._Element) -> str:
|
|
|
159
159
|
return tag
|
|
160
160
|
|
|
161
161
|
|
|
162
|
+
def _paragraph_plain_text_length(paragraph: etree._Element) -> int | None:
|
|
163
|
+
"""Plain text length of a simple paragraph, or ``None`` when unjudgeable.
|
|
164
|
+
|
|
165
|
+
Mirrors the oxml-side stale detector: only paragraphs made purely of runs
|
|
166
|
+
with text/tab-like children can be judged at the byte boundary; anything
|
|
167
|
+
else (controls, tables, fields) returns ``None`` so its cache is kept.
|
|
168
|
+
"""
|
|
169
|
+
total = 0
|
|
170
|
+
for child in paragraph:
|
|
171
|
+
child_name = _local_name(child).lower()
|
|
172
|
+
if child_name in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
173
|
+
continue
|
|
174
|
+
if child_name != "run":
|
|
175
|
+
return None
|
|
176
|
+
for run_child in child:
|
|
177
|
+
run_child_name = _local_name(run_child).lower()
|
|
178
|
+
if run_child_name == "t":
|
|
179
|
+
total += len("".join(run_child.itertext()))
|
|
180
|
+
elif run_child_name in {"tab", "linebreak", "hyphen", "nbspace"}:
|
|
181
|
+
total += 1
|
|
182
|
+
else:
|
|
183
|
+
return None
|
|
184
|
+
return total
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _paragraph_layout_cache_is_stale(paragraph: etree._Element) -> bool:
|
|
188
|
+
text_length = _paragraph_plain_text_length(paragraph)
|
|
189
|
+
if text_length is None:
|
|
190
|
+
return False
|
|
191
|
+
for child in paragraph:
|
|
192
|
+
if _local_name(child).lower() not in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
193
|
+
continue
|
|
194
|
+
for line_seg in child:
|
|
195
|
+
if _local_name(line_seg).lower() != "lineseg":
|
|
196
|
+
continue
|
|
197
|
+
textpos = line_seg.get("textpos")
|
|
198
|
+
if textpos is None:
|
|
199
|
+
continue
|
|
200
|
+
try:
|
|
201
|
+
if int(textpos) > text_length:
|
|
202
|
+
return True
|
|
203
|
+
except ValueError:
|
|
204
|
+
return True
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
162
208
|
def _strip_section_layout_caches(payload: bytes) -> bytes:
|
|
209
|
+
"""Drop only *stale* paragraph layout caches from a section payload.
|
|
210
|
+
|
|
211
|
+
Valid caches are the authored layout of untouched paragraphs; removing
|
|
212
|
+
them forces the editor to re-lay-out entire multi-page documents, which
|
|
213
|
+
shifted page counts and stacked glyphs in the wild form-fill differential
|
|
214
|
+
(specs/031 P0 receipt). The mutating APIs upstream clear the caches of
|
|
215
|
+
exactly the paragraphs they touch, so this byte-boundary sweep only
|
|
216
|
+
guards against writers that left a provably stale cache behind.
|
|
217
|
+
"""
|
|
163
218
|
root = parse_xml(payload)
|
|
164
219
|
removed = False
|
|
165
220
|
for parent in root.iter():
|
|
221
|
+
if _local_name(parent).lower() != "p":
|
|
222
|
+
continue
|
|
223
|
+
if not _paragraph_layout_cache_is_stale(parent):
|
|
224
|
+
continue
|
|
166
225
|
for child in list(parent):
|
|
167
226
|
if _local_name(child).lower() in _LAYOUT_CACHE_ELEMENT_NAMES:
|
|
168
227
|
parent.remove(child)
|