python-hwpx 3.4.1__tar.gz → 3.5.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.5.0}/PKG-INFO +2 -2
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/README.md +1 -1
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/pyproject.toml +1 -1
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/engine.py +225 -3
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/measure.py +155 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0/src/python_hwpx.egg-info}/PKG-INFO +2 -2
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fit.py +122 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fit_integration.py +15 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/LICENSE +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/NOTICE +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/setup.cfg +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/fields.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/bundle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/dump.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/mapping.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/model.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/native.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/blueprint/replay.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/commands.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/form_plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/model.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/path.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/query.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/agent/story.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/authoring.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/benchmark/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/benchmark/blind_eval.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/builder/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/builder/core.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/builder/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/badges.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/corpus/corpus.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/corpus/meeting_summary.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/corpus/notice.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/corpus/report_table.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/roundtrip_batch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/conformance/runner.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/_support.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/composer.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/harvest.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/application_form/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/application_form/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/application_form/fragments/info_table.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/application_form/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/application_form/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/application_form/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/home_notice/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/home_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/home_notice/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/home_notice/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/home_notice/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/official_notice/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/official_notice/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/official_notice/fragments/info_table.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/official_notice/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/official_notice/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/official_notice/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/report/fragments/body.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/report/fragments/heading.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/report/fragments/info_table.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/report/fragments/title.xml +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/report/profile.json +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/profiles/report/template.hwpx +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/design/validator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/evalplan_fill.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/exam/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/exam/compose.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/exam/ir.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/exam/measure.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/exam/parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/exam/profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/fill_residue.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fill.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/seal.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/form_fit/wordbox.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/formfill_quality.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/guidance_scan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/opc/xml_utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/objects.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/section_format.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/section_story.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/table.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/package.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/presets/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/presets/proposal.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/table_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/template_formfit.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/advanced_generators.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/fuzz/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/fuzz/__main__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/fuzz/catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/fuzz/generator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/fuzz/minimize.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/fuzz/runner.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/official_lint.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/pii.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/report_parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/style_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/table_compute.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/table_navigation.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/__init__.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/_hancom_open_rate.ps1 +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/_refresh_hwpx_mac.applescript +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/_render_hwpx.ps1 +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/_render_hwpx_mac.applescript +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/detectors.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/fixture_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/hancom_worker.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/masks.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/page_qa.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/qa_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/qa_metrics.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/hwpx/visual/report.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/python_hwpx.egg-info/SOURCES.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_advanced_generators.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_blueprint_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_blueprint_dump.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_blueprint_replay.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_catalog.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_cli.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_commands.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_document.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_mixed_form.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_path_query.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_agent_story_commands.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_authoring_native_toc.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_authoring_profile_routing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_authoring_profile_routing_oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_authoring_render_check.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_body_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_builder_core.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_builder_plan_v2.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_builder_vertical_slice.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_conformance.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_corpus_toc_verify.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_design_builder.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_document_plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_document_plan_computed_fields.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_evalplan_fill.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_compose.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_compose_oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_fixtures.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_ir.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_measure.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_exam_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_fill_residue.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fill_split_run.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fit_seal.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fit_seal_placement.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_form_fit_wordbox.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_formfill_differential_driver.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_formfill_quality.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_government_report_preset.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_government_table_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_guidance_scan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_hancom_render_worker.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_ingest.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_kordoc_absorption.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_m9_p0_spike_tools.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_mail_merge_fit.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_mail_merge_table_compute.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_mixed_form_plan.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_new_features.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_official_document_style.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_official_lint_gongmun_gate.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_official_lint_tableaware.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_openrate.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_pii.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_pii_leak_sweep.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_pii_structural.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_pii_wiring.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_proposal_preset.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_question_split_detector.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_report_parser.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_rhwp_t1_gates.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_rhwp_t2_verification.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_save_pipeline_no_bypass.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_style_profile.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_table_navigation.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_template_formfit.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_version_metadata.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_visual_fixture_corpus.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_visual_oracle.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_visual_oracle_budget.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.0}/tests/test_visual_qa_contracts.py +0 -0
- {python_hwpx-3.4.1 → python_hwpx-3.5.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.5.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.5.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.5.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.5.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:
|
|
@@ -63,7 +82,7 @@ class FitEngine:
|
|
|
63
82
|
wrap_slot = replace(slot, max_lines=wrap_lines)
|
|
64
83
|
measurement = measure(value, wrap_slot)
|
|
65
84
|
if measurement.fits:
|
|
66
|
-
|
|
85
|
+
result = FitResult(
|
|
67
86
|
ok=True,
|
|
68
87
|
value=value,
|
|
69
88
|
applied_value=value,
|
|
@@ -78,11 +97,15 @@ class FitEngine:
|
|
|
78
97
|
warnings=list(measurement.notes),
|
|
79
98
|
field_id=field_id,
|
|
80
99
|
)
|
|
100
|
+
return self._apply_height_budget(result, value, slot, policy, wrap_lines, field_id)
|
|
81
101
|
|
|
82
102
|
if policy.may_shrink:
|
|
83
103
|
shrunk = self._try_shrink(value, slot, policy, wrap_lines)
|
|
84
104
|
if shrunk is not None:
|
|
85
|
-
|
|
105
|
+
shrunk = replace(shrunk, field_id=field_id)
|
|
106
|
+
return self._apply_height_budget(
|
|
107
|
+
shrunk, value, slot, policy, wrap_lines, field_id
|
|
108
|
+
)
|
|
86
109
|
|
|
87
110
|
# Still overflowing → terminal overflow action (confidence-gated).
|
|
88
111
|
return self._overflow(value, wrap_slot, policy, field_id, measurement)
|
|
@@ -212,6 +235,205 @@ class FitEngine:
|
|
|
212
235
|
candidate = round(candidate - _SHRINK_STEP_PT, 4)
|
|
213
236
|
return best
|
|
214
237
|
|
|
238
|
+
# -- vertical (row-height) budget ------------------------------------- #
|
|
239
|
+
def _apply_height_budget(
|
|
240
|
+
self,
|
|
241
|
+
result: FitResult,
|
|
242
|
+
value: str,
|
|
243
|
+
slot: SlotMetrics,
|
|
244
|
+
policy: FitPolicy,
|
|
245
|
+
wrap_lines: int,
|
|
246
|
+
field_id: str | None,
|
|
247
|
+
) -> FitResult:
|
|
248
|
+
"""Overlay the authored row-height budget on a width-fitting *result*.
|
|
249
|
+
|
|
250
|
+
The width ladder decides horizontal fit; this decides whether the value
|
|
251
|
+
also sits inside the cell's *vertical* room, so a filled row does not
|
|
252
|
+
silently grow and shift the page (the dominant M9 form-fill defect).
|
|
253
|
+
|
|
254
|
+
Honesty contract (mirrors the width path, grounded in the S-085 P0/P1
|
|
255
|
+
measurement): the authored ``cellSz.height`` is an unreliable proxy in wild
|
|
256
|
+
forms (auto-grow, tiny floors, merges), so a *modest* vertical overflow is
|
|
257
|
+
reported and deferred to the render oracle; only a *gross* balloon shrinks
|
|
258
|
+
or fails closed. ``allow_row_expand`` / ``expand_row`` opt out entirely.
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
if slot.available_height is None:
|
|
262
|
+
if slot.height_unavailable and result.ok:
|
|
263
|
+
result.warnings.append(_HEIGHT_UNAVAILABLE_NOTE)
|
|
264
|
+
return result
|
|
265
|
+
if policy.allow_row_expand or policy.mode == "expand_row":
|
|
266
|
+
return result
|
|
267
|
+
|
|
268
|
+
font = result.font_pt if result.font_pt is not None else slot.font_pt
|
|
269
|
+
wrapped = result.lines if result.lines is not None else 1
|
|
270
|
+
verdict = self._height_verdict(slot, wrapped, font)
|
|
271
|
+
if verdict == "fits":
|
|
272
|
+
return result
|
|
273
|
+
if verdict == "modest":
|
|
274
|
+
# S-085 P1 calibration round 2 (differential-measured): the modest
|
|
275
|
+
# band is where pages actually shift, so try to shrink INTO the
|
|
276
|
+
# budget first; only defer to the oracle when no font >= min can
|
|
277
|
+
# land the value fully inside the authored height.
|
|
278
|
+
if policy.may_shrink:
|
|
279
|
+
shrunk = self._try_shrink_for_height(
|
|
280
|
+
value, slot, policy, wrap_lines, field_id, accept_modest=False
|
|
281
|
+
)
|
|
282
|
+
if shrunk is not None:
|
|
283
|
+
return shrunk
|
|
284
|
+
result.overflow_detected = True
|
|
285
|
+
result.warnings.append(_ROW_GROWTH_NOTE)
|
|
286
|
+
return result
|
|
287
|
+
|
|
288
|
+
# Gross vertical balloon: shrink (prefer a full fit, else the least
|
|
289
|
+
# growth), else fail-closed.
|
|
290
|
+
if policy.may_shrink:
|
|
291
|
+
shrunk = self._try_shrink_for_height(
|
|
292
|
+
value, slot, policy, wrap_lines, field_id, accept_modest=True
|
|
293
|
+
)
|
|
294
|
+
if shrunk is not None:
|
|
295
|
+
return shrunk
|
|
296
|
+
return self._overflow_height(value, slot, policy, field_id, wrapped, font)
|
|
297
|
+
|
|
298
|
+
def _height_verdict(self, slot: SlotMetrics, wrapped_lines: int, font_pt: float) -> str:
|
|
299
|
+
"""Classify vertical fit as ``fits`` / ``modest`` / ``gross``."""
|
|
300
|
+
|
|
301
|
+
budget = slot.height_lines(font_pt)
|
|
302
|
+
if budget is None or wrapped_lines <= budget:
|
|
303
|
+
return "fits"
|
|
304
|
+
optimistic = slot.height_lines_optimistic(font_pt) or budget
|
|
305
|
+
if (
|
|
306
|
+
wrapped_lines > optimistic * GROSS_ROW_GROWTH_FACTOR
|
|
307
|
+
and (wrapped_lines - optimistic) >= MIN_ROW_GROWTH_LINES
|
|
308
|
+
):
|
|
309
|
+
return "gross"
|
|
310
|
+
return "modest"
|
|
311
|
+
|
|
312
|
+
def _try_shrink_for_height(
|
|
313
|
+
self,
|
|
314
|
+
value: str,
|
|
315
|
+
slot: SlotMetrics,
|
|
316
|
+
policy: FitPolicy,
|
|
317
|
+
wrap_lines: int,
|
|
318
|
+
field_id: str | None,
|
|
319
|
+
*,
|
|
320
|
+
accept_modest: bool,
|
|
321
|
+
) -> FitResult | None:
|
|
322
|
+
"""Largest font (>= min) that lands the value INSIDE the height budget.
|
|
323
|
+
|
|
324
|
+
Walks the shrink ladder preferring a full vertical fit ("fits"). When
|
|
325
|
+
``accept_modest`` is true and no font achieves a full fit, the largest
|
|
326
|
+
candidate whose growth is merely *modest* (never gross) is returned as
|
|
327
|
+
the least-damage fallback, reported and deferred to the oracle.
|
|
328
|
+
"""
|
|
329
|
+
|
|
330
|
+
ceiling = policy.max_font_pt or slot.font_pt
|
|
331
|
+
candidate = min(slot.font_pt, ceiling)
|
|
332
|
+
modest_candidate: tuple[float, int] | None = None
|
|
333
|
+
while candidate >= policy.min_font_pt - 1e-9:
|
|
334
|
+
trial = replace(slot, font_pt=candidate, max_lines=wrap_lines)
|
|
335
|
+
m = measure(value, trial)
|
|
336
|
+
if m.fits and m.confidence == "high":
|
|
337
|
+
verdict = self._height_verdict(slot, m.lines, candidate)
|
|
338
|
+
if verdict == "fits":
|
|
339
|
+
return self._shrunk_result(
|
|
340
|
+
value, slot, candidate, m.lines, wrap_lines, field_id,
|
|
341
|
+
modest=False,
|
|
342
|
+
)
|
|
343
|
+
if verdict == "modest" and modest_candidate is None:
|
|
344
|
+
modest_candidate = (candidate, m.lines)
|
|
345
|
+
candidate = round(candidate - _SHRINK_STEP_PT, 4)
|
|
346
|
+
if accept_modest and modest_candidate is not None:
|
|
347
|
+
font_pt, lines = modest_candidate
|
|
348
|
+
return self._shrunk_result(
|
|
349
|
+
value, slot, font_pt, lines, wrap_lines, field_id, modest=True
|
|
350
|
+
)
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
def _shrunk_result(
|
|
354
|
+
self,
|
|
355
|
+
value: str,
|
|
356
|
+
slot: SlotMetrics,
|
|
357
|
+
candidate: float,
|
|
358
|
+
lines: int,
|
|
359
|
+
wrap_lines: int,
|
|
360
|
+
field_id: str | None,
|
|
361
|
+
*,
|
|
362
|
+
modest: bool,
|
|
363
|
+
) -> FitResult:
|
|
364
|
+
changes: dict[str, object] = {}
|
|
365
|
+
warnings: list[str] = []
|
|
366
|
+
if candidate != slot.font_pt:
|
|
367
|
+
changes = {
|
|
368
|
+
"font_pt": round(candidate, 2),
|
|
369
|
+
"from_font_pt": round(slot.font_pt, 2),
|
|
370
|
+
}
|
|
371
|
+
warnings.append(
|
|
372
|
+
f"font shrunk {slot.font_pt:g}pt → {candidate:g}pt to fit the row height"
|
|
373
|
+
)
|
|
374
|
+
if wrap_lines > 1 and lines > 1:
|
|
375
|
+
changes["wrapped_lines"] = lines
|
|
376
|
+
if modest:
|
|
377
|
+
warnings.append(_ROW_GROWTH_NOTE)
|
|
378
|
+
return FitResult(
|
|
379
|
+
ok=True,
|
|
380
|
+
value=value,
|
|
381
|
+
applied_value=value,
|
|
382
|
+
applied_style_changes=changes,
|
|
383
|
+
lines=lines,
|
|
384
|
+
font_pt=round(candidate, 2),
|
|
385
|
+
confidence="high",
|
|
386
|
+
overflow_detected=modest,
|
|
387
|
+
warnings=warnings,
|
|
388
|
+
field_id=field_id,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
def _overflow_height(
|
|
392
|
+
self,
|
|
393
|
+
value: str,
|
|
394
|
+
slot: SlotMetrics,
|
|
395
|
+
policy: FitPolicy,
|
|
396
|
+
field_id: str | None,
|
|
397
|
+
wrapped_lines: int,
|
|
398
|
+
font_pt: float,
|
|
399
|
+
) -> FitResult:
|
|
400
|
+
"""Terminal action for a gross vertical balloon that shrink cannot resolve."""
|
|
401
|
+
|
|
402
|
+
budget = slot.height_lines(font_pt)
|
|
403
|
+
action = policy.overflow
|
|
404
|
+
# Line-level truncation is not modelled; keep the value and warn (honest).
|
|
405
|
+
if action == "truncate":
|
|
406
|
+
action = "warn"
|
|
407
|
+
if action == "warn":
|
|
408
|
+
return FitResult(
|
|
409
|
+
ok=True,
|
|
410
|
+
value=value,
|
|
411
|
+
applied_value=value,
|
|
412
|
+
lines=wrapped_lines,
|
|
413
|
+
font_pt=font_pt,
|
|
414
|
+
overflow_detected=True,
|
|
415
|
+
confidence="high",
|
|
416
|
+
warnings=[
|
|
417
|
+
f"value needs ~{wrapped_lines} lines but the row height holds "
|
|
418
|
+
f"~{budget}; the row will balloon (overflow=warn)"
|
|
419
|
+
],
|
|
420
|
+
field_id=field_id,
|
|
421
|
+
)
|
|
422
|
+
return FitResult(
|
|
423
|
+
ok=False,
|
|
424
|
+
value=value,
|
|
425
|
+
applied_value=value,
|
|
426
|
+
lines=wrapped_lines,
|
|
427
|
+
font_pt=font_pt,
|
|
428
|
+
overflow_detected=True,
|
|
429
|
+
confidence="high",
|
|
430
|
+
errors=[
|
|
431
|
+
f"FIELD_OVERFLOW: value needs ~{wrapped_lines} lines but the row height "
|
|
432
|
+
f"holds ~{budget}; shrink/shorten the value or allow row expansion"
|
|
433
|
+
],
|
|
434
|
+
field_id=field_id,
|
|
435
|
+
)
|
|
436
|
+
|
|
215
437
|
# -- terminal overflow action ----------------------------------------- #
|
|
216
438
|
def _overflow(
|
|
217
439
|
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,62 @@ 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
|
+
# A cell height existed but was unusable (merged row-span fragment, or an
|
|
225
|
+
# auto-grow floor shorter than one line). Records "height budget unavailable"
|
|
226
|
+
# so the fit reports width-only honestly rather than guessing a vertical fit.
|
|
227
|
+
height_unavailable: bool = False
|
|
192
228
|
|
|
193
229
|
@property
|
|
194
230
|
def capacity(self) -> float:
|
|
195
231
|
return self.available_width * self.max_lines
|
|
196
232
|
|
|
233
|
+
def _line_ratio(self) -> float:
|
|
234
|
+
ratio = self.line_spacing_ratio
|
|
235
|
+
return ratio if ratio and ratio > 0 else DEFAULT_LINE_SPACING_RATIO
|
|
236
|
+
|
|
237
|
+
def line_height(self, font_pt: float | None = None) -> float:
|
|
238
|
+
"""Expected per-line vertical advance in HWPUNIT at *font_pt*."""
|
|
239
|
+
|
|
240
|
+
pt = self.font_pt if font_pt is None else font_pt
|
|
241
|
+
return pt * 100.0 * self._line_ratio()
|
|
242
|
+
|
|
243
|
+
def height_lines(self, font_pt: float | None = None) -> int | None:
|
|
244
|
+
"""Expected vertical line budget at *font_pt* (declared/default pitch).
|
|
245
|
+
|
|
246
|
+
``None`` when the vertical room is unmeasured. Never less than 1 — a cell
|
|
247
|
+
always accommodates its first line; the risk we guard is *growth* past it.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
if self.available_height is None:
|
|
251
|
+
return None
|
|
252
|
+
line_h = self.line_height(font_pt)
|
|
253
|
+
if line_h <= 0:
|
|
254
|
+
return None
|
|
255
|
+
return max(int(self.available_height // line_h), 1)
|
|
256
|
+
|
|
257
|
+
def height_lines_optimistic(self, font_pt: float | None = None) -> int | None:
|
|
258
|
+
"""Most-generous vertical budget (tightest plausible pitch).
|
|
259
|
+
|
|
260
|
+
This is the basis for the *confidence* of a vertical overflow: content that
|
|
261
|
+
overflows even this budget is grossly too tall regardless of pitch error.
|
|
262
|
+
"""
|
|
263
|
+
|
|
264
|
+
if self.available_height is None:
|
|
265
|
+
return None
|
|
266
|
+
pt = self.font_pt if font_pt is None else font_pt
|
|
267
|
+
ratio = min(self._line_ratio(), MIN_LINE_SPACING_RATIO)
|
|
268
|
+
line_h = pt * 100.0 * ratio
|
|
269
|
+
if line_h <= 0:
|
|
270
|
+
return None
|
|
271
|
+
return max(int(self.available_height // line_h), 1)
|
|
272
|
+
|
|
197
273
|
|
|
198
274
|
@dataclass(slots=True)
|
|
199
275
|
class Measurement:
|
|
@@ -291,6 +367,50 @@ def _cell_margin(cell_element: object) -> tuple[int, int]:
|
|
|
291
367
|
return (0, 0)
|
|
292
368
|
|
|
293
369
|
|
|
370
|
+
def _cell_margin_vertical(cell_element: object) -> tuple[int, int]:
|
|
371
|
+
"""Return (top, bottom) cellMargin in HWPUNIT, defaulting to 0."""
|
|
372
|
+
|
|
373
|
+
for child in cell_element: # type: ignore[attr-defined]
|
|
374
|
+
if _local_name(child.tag) == "cellMargin":
|
|
375
|
+
return (
|
|
376
|
+
int(child.get("top", "0") or 0),
|
|
377
|
+
int(child.get("bottom", "0") or 0),
|
|
378
|
+
)
|
|
379
|
+
return (0, 0)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _first_para_line_spacing_ratio(cell: object, document: object) -> float | None:
|
|
383
|
+
"""Per-line em multiple from the cell's first paragraph line spacing (PERCENT).
|
|
384
|
+
|
|
385
|
+
Only PERCENT spacing maps cleanly onto the em-relative line-height model; FIXED
|
|
386
|
+
/ ATLEAST / BETWEENLINES are left to the conservative default so we never invent
|
|
387
|
+
a font-independent pitch (the shrink ladder varies the font).
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
try:
|
|
391
|
+
paragraphs = cell.paragraphs # type: ignore[attr-defined]
|
|
392
|
+
except Exception: # pragma: no cover - defensive
|
|
393
|
+
return None
|
|
394
|
+
for paragraph in paragraphs:
|
|
395
|
+
ref = getattr(paragraph, "para_pr_id_ref", None)
|
|
396
|
+
if ref is None or document is None:
|
|
397
|
+
continue
|
|
398
|
+
try:
|
|
399
|
+
prop = document.paragraph_property(ref) # type: ignore[attr-defined]
|
|
400
|
+
except Exception: # pragma: no cover - defensive
|
|
401
|
+
prop = None
|
|
402
|
+
spacing = getattr(prop, "line_spacing", None) if prop is not None else None
|
|
403
|
+
if spacing is None or not getattr(spacing, "value", None):
|
|
404
|
+
continue
|
|
405
|
+
if (getattr(spacing, "spacing_type", None) or "PERCENT").upper() != "PERCENT":
|
|
406
|
+
return None
|
|
407
|
+
try:
|
|
408
|
+
return int(spacing.value) / 100.0
|
|
409
|
+
except (TypeError, ValueError): # pragma: no cover - defensive
|
|
410
|
+
return None
|
|
411
|
+
return None
|
|
412
|
+
|
|
413
|
+
|
|
294
414
|
def _first_run_font_pt(cell: object, document: object) -> float:
|
|
295
415
|
"""Resolve the cell's first run font size in points (default 10pt)."""
|
|
296
416
|
|
|
@@ -339,6 +459,14 @@ def resolve_slot_metrics(
|
|
|
339
459
|
verified against Hancom's own ``lineSeg/@horzsize`` (±10 HWPUNIT on 82% of
|
|
340
460
|
cells; the safety factor covers the rest plus paragraph indent, which is left
|
|
341
461
|
to the HarfBuzz pass).
|
|
462
|
+
|
|
463
|
+
``available_height`` follows the same philosophy — ``(cellSz.height - top -
|
|
464
|
+
bottom margin) * safety`` — but is recorded as *unavailable* (``None`` +
|
|
465
|
+
``height_unavailable``) whenever the authored height is not a real ceiling:
|
|
466
|
+
a merged cell (its ``cellSz.height`` is a single-row fragment, not the spanned
|
|
467
|
+
height), a cell with no height, or an auto-grow floor shorter than one line
|
|
468
|
+
(Hancom simply grows the row past it). The fit then stays width-only rather
|
|
469
|
+
than guess a vertical fit.
|
|
342
470
|
"""
|
|
343
471
|
|
|
344
472
|
raw_width = float(getattr(cell, "width", 0) or 0)
|
|
@@ -346,12 +474,35 @@ def resolve_slot_metrics(
|
|
|
346
474
|
left, right = _cell_margin(element) if element is not None else (0, 0)
|
|
347
475
|
inner = max(raw_width - left - right, 0.0) * safety
|
|
348
476
|
resolved_pt = font_pt if font_pt is not None else _first_run_font_pt(cell, document)
|
|
477
|
+
line_ratio = _first_para_line_spacing_ratio(cell, document)
|
|
478
|
+
|
|
479
|
+
available_height: float | None = None
|
|
480
|
+
height_unavailable = False
|
|
481
|
+
raw_height = float(getattr(cell, "height", 0) or 0)
|
|
482
|
+
try:
|
|
483
|
+
row_span = int(getattr(cell, "span", (1, 1))[0])
|
|
484
|
+
except Exception: # pragma: no cover - defensive
|
|
485
|
+
row_span = 1
|
|
486
|
+
top, bottom = _cell_margin_vertical(element) if element is not None else (0, 0)
|
|
487
|
+
inner_h = max(raw_height - top - bottom, 0.0) * safety if raw_height > 0 else 0.0
|
|
488
|
+
# A cell authored shorter than one line at the tightest pitch is an auto-grow
|
|
489
|
+
# floor, not a ceiling (Hancom grows the row past it); a merged row-span's
|
|
490
|
+
# cellSz.height is only one of the spanned rows. Neither is a usable budget.
|
|
491
|
+
one_line = resolved_pt * 100.0 * MIN_LINE_SPACING_RATIO
|
|
492
|
+
if row_span <= 1 and inner_h >= one_line:
|
|
493
|
+
available_height = inner_h
|
|
494
|
+
else:
|
|
495
|
+
height_unavailable = True
|
|
496
|
+
|
|
349
497
|
return SlotMetrics(
|
|
350
498
|
available_width=inner,
|
|
351
499
|
font_pt=resolved_pt,
|
|
352
500
|
max_lines=max(max_lines, 1),
|
|
353
501
|
raw_width=raw_width,
|
|
354
502
|
source="cell",
|
|
503
|
+
available_height=available_height,
|
|
504
|
+
line_spacing_ratio=line_ratio,
|
|
505
|
+
height_unavailable=height_unavailable,
|
|
355
506
|
)
|
|
356
507
|
|
|
357
508
|
|
|
@@ -360,6 +511,10 @@ __all__ = [
|
|
|
360
511
|
"Measurement",
|
|
361
512
|
"Confidence",
|
|
362
513
|
"DEFAULT_SAFETY",
|
|
514
|
+
"DEFAULT_LINE_SPACING_RATIO",
|
|
515
|
+
"MIN_LINE_SPACING_RATIO",
|
|
516
|
+
"GROSS_ROW_GROWTH_FACTOR",
|
|
517
|
+
"MIN_ROW_GROWTH_LINES",
|
|
363
518
|
"classify_char",
|
|
364
519
|
"char_advance",
|
|
365
520
|
"estimate_text_width",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.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.5.0`입니다. 일반
|
|
74
74
|
`pip install python-hwpx`로 이 릴리스를 설치할 수 있습니다.
|
|
75
75
|
현재 패키지 분류는 `Development Status :: 3 - Alpha`입니다. 이 분류는 API와 제품의
|
|
76
76
|
성숙도를 나타내며, 공개 버전이나 플러그인의 최소 호환 버전을 대신하지 않습니다.
|
|
@@ -200,6 +200,99 @@ def test_warn_policy_never_fails():
|
|
|
200
200
|
assert result.ok is True and result.overflow_detected is True
|
|
201
201
|
|
|
202
202
|
|
|
203
|
+
# --------------------------------------------------------------------------- #
|
|
204
|
+
# Vertical (row-height) budget — S-085 P1.
|
|
205
|
+
# --------------------------------------------------------------------------- #
|
|
206
|
+
def _vslot(width=6000, font_pt=10.0, height=None, ratio=None, unavailable=False):
|
|
207
|
+
return SlotMetrics(
|
|
208
|
+
available_width=width,
|
|
209
|
+
font_pt=font_pt,
|
|
210
|
+
available_height=height,
|
|
211
|
+
line_spacing_ratio=ratio,
|
|
212
|
+
height_unavailable=unavailable,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_line_height_and_budget_math():
|
|
217
|
+
slot = _vslot(height=3300) # default 160% pitch → 1600/line; tight 1.0 → 1000/line
|
|
218
|
+
assert slot.line_height(10.0) == pytest.approx(1600.0)
|
|
219
|
+
assert slot.height_lines(10.0) == 2 # floor(3300 / 1600)
|
|
220
|
+
assert slot.height_lines_optimistic(10.0) == 3 # floor(3300 / 1000)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_budget_uses_declared_percent_line_spacing():
|
|
224
|
+
slot = _vslot(height=3300, ratio=2.0) # declared 200% → 2000/line
|
|
225
|
+
assert slot.line_height(10.0) == pytest.approx(2000.0)
|
|
226
|
+
assert slot.height_lines(10.0) == 1 # floor(3300 / 2000)
|
|
227
|
+
# Optimistic budget never uses a looser pitch than the tight floor.
|
|
228
|
+
assert slot.height_lines_optimistic(10.0) == 3 # floor(3300 / 1000)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def test_budget_is_none_without_height_and_never_below_one():
|
|
232
|
+
assert _vslot(height=None).height_lines(10.0) is None
|
|
233
|
+
assert _vslot(height=None).height_lines_optimistic(10.0) is None
|
|
234
|
+
# A cell shorter than one line still reports a 1-line budget (guaranteed floor).
|
|
235
|
+
assert _vslot(height=500).height_lines(10.0) == 1
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_no_height_budget_is_width_only_with_warning_when_unavailable():
|
|
239
|
+
engine = FitEngine()
|
|
240
|
+
warned = engine.fit("홍길동", _vslot(height=None, unavailable=True), FitPolicy())
|
|
241
|
+
assert warned.ok and any("width-only" in w for w in warned.warnings)
|
|
242
|
+
# A caller that never had a height (native field) must NOT get the warning.
|
|
243
|
+
silent = engine.fit("홍길동", _vslot(height=None, unavailable=False), FitPolicy())
|
|
244
|
+
assert silent.ok and not any("width-only" in w for w in silent.warnings)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_value_fits_within_ample_height_is_unchanged():
|
|
248
|
+
engine = FitEngine()
|
|
249
|
+
# 11 Hangul wrap to 2 lines in a 6000-wide slot; a 6000-tall cell budgets 3.
|
|
250
|
+
result = engine.fit("가" * 11, _vslot(height=6000), FitPolicy(mode="wrap"))
|
|
251
|
+
assert result.ok and not result.overflow_detected
|
|
252
|
+
assert not any("row will grow" in w for w in result.warnings)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def test_modest_vertical_overflow_warns_and_defers_to_oracle():
|
|
256
|
+
engine = FitEngine()
|
|
257
|
+
# 2 lines needed, 1-line height budget → modest growth: reported, never a fail.
|
|
258
|
+
result = engine.fit("가" * 11, _vslot(height=1600), FitPolicy(mode="wrap"))
|
|
259
|
+
assert result.ok is True
|
|
260
|
+
assert result.overflow_detected is True
|
|
261
|
+
assert result.errors == []
|
|
262
|
+
assert any("row will grow" in w for w in result.warnings)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def test_gross_vertical_balloon_fails_closed_when_cannot_shrink():
|
|
266
|
+
engine = FitEngine()
|
|
267
|
+
# 4 lines in a 1-line budget = a gross balloon; wrap mode cannot shrink → fail.
|
|
268
|
+
result = engine.fit(
|
|
269
|
+
"가" * 20, _vslot(height=1600), FitPolicy(mode="wrap", overflow="fail")
|
|
270
|
+
)
|
|
271
|
+
assert result.ok is False and result.overflow_detected
|
|
272
|
+
assert any("FIELD_OVERFLOW" in e for e in result.errors)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_gross_vertical_balloon_shrinks_when_allowed():
|
|
276
|
+
engine = FitEngine()
|
|
277
|
+
before = _vslot(height=1600)
|
|
278
|
+
result = engine.fit("가" * 20, before, FitPolicy(mode="wrap_then_shrink", min_font_pt=8.0))
|
|
279
|
+
assert result.ok is True
|
|
280
|
+
assert result.font_pt is not None and result.font_pt < before.font_pt
|
|
281
|
+
assert result.applied_style_changes.get("from_font_pt") == 10.0
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def test_expand_row_bypasses_the_height_budget():
|
|
285
|
+
engine = FitEngine()
|
|
286
|
+
# A gross balloon is accepted (no fail) when row growth is opted into explicitly.
|
|
287
|
+
result = engine.fit(
|
|
288
|
+
"가" * 20,
|
|
289
|
+
_vslot(height=1600),
|
|
290
|
+
FitPolicy(mode="wrap_then_shrink", allow_row_expand=True),
|
|
291
|
+
)
|
|
292
|
+
assert result.ok is True
|
|
293
|
+
assert not any("FIELD_OVERFLOW" in e for e in result.errors)
|
|
294
|
+
|
|
295
|
+
|
|
203
296
|
# --------------------------------------------------------------------------- #
|
|
204
297
|
# Report adapter → the one VisualCompleteReport.
|
|
205
298
|
# --------------------------------------------------------------------------- #
|
|
@@ -215,3 +308,32 @@ def test_to_form_report_folds_results():
|
|
|
215
308
|
assert len(report.fields) == 2
|
|
216
309
|
assert report.fields[0]["fieldId"] == "name"
|
|
217
310
|
assert any("FIELD_OVERFLOW" in e for e in report.errors)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_modest_vertical_overflow_shrinks_into_budget_when_possible():
|
|
314
|
+
# S-085 P1 round 2 (differential-measured): the modest band is where pages
|
|
315
|
+
# actually shift, so wrap_then_shrink must land INSIDE the height budget
|
|
316
|
+
# instead of warning and letting the row grow.
|
|
317
|
+
engine = FitEngine()
|
|
318
|
+
# height=2400: 2 lines exceed the 1-line budget at 10pt (modest), but a
|
|
319
|
+
# shrink to 7.5pt lands both lines fully inside the budget.
|
|
320
|
+
before = _vslot(height=2400)
|
|
321
|
+
result = engine.fit(
|
|
322
|
+
"가" * 11, before, FitPolicy(mode="wrap_then_shrink", min_font_pt=6.0)
|
|
323
|
+
)
|
|
324
|
+
assert result.ok is True
|
|
325
|
+
assert result.overflow_detected is False
|
|
326
|
+
assert not any("row will grow" in w for w in result.warnings)
|
|
327
|
+
assert result.font_pt is not None and result.font_pt < before.font_pt
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def test_modest_vertical_overflow_defers_when_shrink_cannot_reach_budget():
|
|
331
|
+
# height=1600 cannot hold two lines even at min font — the honest outcome
|
|
332
|
+
# stays the reported modest deferral, never a false shrink claim.
|
|
333
|
+
engine = FitEngine()
|
|
334
|
+
result = engine.fit(
|
|
335
|
+
"가" * 11, _vslot(height=1600), FitPolicy(mode="wrap_then_shrink", min_font_pt=6.0)
|
|
336
|
+
)
|
|
337
|
+
assert result.ok is True
|
|
338
|
+
assert result.overflow_detected is True
|
|
339
|
+
assert any("row will grow" in w for w in result.warnings)
|
|
@@ -84,6 +84,21 @@ def test_fit_records_ledger_entry_on_style_change():
|
|
|
84
84
|
assert entry.reason in ("style_changed", "form_filled")
|
|
85
85
|
|
|
86
86
|
|
|
87
|
+
def test_fit_row_height_balloon_refuses_through_real_cell():
|
|
88
|
+
# A real cell with a short authored height: a grossly too-tall value must not be
|
|
89
|
+
# accepted as a silent 100-line row growth — it fails closed (S-085 P1).
|
|
90
|
+
doc = HwpxDocument.new()
|
|
91
|
+
table = doc.add_table(1, 1)
|
|
92
|
+
cell = table.cell(0, 0)
|
|
93
|
+
cell.set_size(width=6000, height=1800) # ~1-line-tall cell
|
|
94
|
+
result = table.set_cell_text(
|
|
95
|
+
0, 0, "서울특별시 강남구 테헤란로 " * 8,
|
|
96
|
+
fit=FitPolicy(mode="wrap_then_shrink", overflow="fail", min_font_pt=8.0),
|
|
97
|
+
)
|
|
98
|
+
assert result is not None and result.ok is False
|
|
99
|
+
assert result.overflow_detected and any("FIELD_OVERFLOW" in e for e in result.errors)
|
|
100
|
+
|
|
101
|
+
|
|
87
102
|
def test_fit_borderline_overflow_downgrades_to_warn():
|
|
88
103
|
# ~5 Hangul (5000) into a slot whose inner width lands just under that after
|
|
89
104
|
# the 0.93 safety factor → borderline, must NOT hard-fail.
|
|
File without changes
|