python-hwpx 6.3.0__tar.gz → 6.4.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-6.3.0/src/python_hwpx.egg-info → python_hwpx-6.4.0}/PKG-INFO +29 -17
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/README.md +28 -16
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/pyproject.toml +3 -1
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/fields.py +7 -12
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/capabilities.py +2 -2
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/data/contract_docs/mutation-semantics.md +13 -7
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/data/contract_docs/recipes-traversal.md +13 -6
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/data/contract_docs/support-matrix.md +2 -2
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/form_fit/measure.py +22 -2
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/opc/xml_utils.py +29 -11
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/objects.py +27 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/section_format.py +19 -12
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/section_story.py +79 -1
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/table.py +76 -1
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/table_patch.py +24 -4
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/table_navigation.py +29 -19
- {python_hwpx-6.3.0 → python_hwpx-6.4.0/src/python_hwpx.egg-info}/PKG-INFO +29 -17
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/python_hwpx.egg-info/SOURCES.txt +3 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_documentation_code_fences.py +48 -2
- python_hwpx-6.4.0/tests/test_edit_fidelity_corpus.py +255 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_form_fit_integration.py +1 -0
- python_hwpx-6.4.0/tests/test_llms_txt.py +124 -0
- python_hwpx-6.4.0/tests/test_native_edit_contract.py +338 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_release_workflow_safety.py +18 -0
- python_hwpx-6.4.0/tests/test_safe_edit_example.py +129 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_navigation.py +33 -0
- python_hwpx-6.3.0/tests/test_llms_txt.py +0 -62
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/LICENSE +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/NOTICE +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/setup.cfg +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/_legacy.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/_resolve.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/_units.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/headings.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/highlight.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/layout.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/media.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/memos.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/_base.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/fields.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/media.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/notes.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/page.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/parts.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/refs.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/shapes.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/styles.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/tables.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/text.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/ns/tracking.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/persistence.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/shapes.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/_document/tracked.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/body_patch.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/data/Skeleton.hwpx +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/data/contract_docs/known-traps.md +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/document.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/equation/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/equation/authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/equation/eqedit.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/equation/mathml.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/equation/render.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/equation/tokens.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/errors.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/experimental.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/form_fit/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/form_fit/apply.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/form_fit/engine.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/form_fit/policy.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/form_fit/report.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/ingest/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/ingest/base.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/ingest/hwpx_converter.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/layout/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/layout/lint.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/layout/report.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/model.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/mutation_report.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/binary_item.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/checkbox.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/form_field.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/highlight.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/results.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/objects/tracked.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/opc/package.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/opc/relationships.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/opc/security.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/_document_impl.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/_document_primitives.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/body.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/canonical_defaults.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/color.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/common.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/document.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/document_metadata.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/document_parts.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/drop_cap.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/dutmal_compose.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/field_marks.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/header.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/header_compat.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/header_part.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/history_part.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/master_page.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/master_page_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/memo.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/namespaces.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/note_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/numbering.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/numbering_kinds.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/paragraph.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/parser.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/run.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/schema.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/section.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/section_layout.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/settings.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/simple_parts.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/utils.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/oxml/version_part.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/package.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/patch.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/plan/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/plan/_execute.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/plan/_model.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/plan/_schema.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/plan/_validate.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/py.typed +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/quality/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/quality/ledger.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/quality/policy.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/quality/rendering.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/quality/report.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/quality/save_pipeline.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/templates.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/__init__.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/_schemas/header.xsd +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/_schemas/section.xsd +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/archive_cli.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/doc_diff.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/document_merge.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/document_viewer.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/exporter.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/generic_inventory.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/id_integrity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/idempotence.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/ir_equality.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/layout_preview.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/mail_merge.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/markdown_export.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/object_finder.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/package_reconcile.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/package_validator.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/page_guard.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/read_fidelity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/recover.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/redline.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/repair.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/report_utils.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/roundtrip_diff.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/table_cleanup.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/template_analyzer.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/text_extract_cli.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/text_extractor.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/toc_author.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/toc_fidelity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/hwpx/tools/validator.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/python_hwpx.egg-info/dependency_links.txt +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/python_hwpx.egg-info/entry_points.txt +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/python_hwpx.egg-info/requires.txt +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/src/python_hwpx.egg-info/top_level.txt +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_add_control.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_add_heading.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_adoption_metrics_snapshot.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_arc_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_authoring_defaults.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_authoring_quality_corpus.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_blind_eval_fixture.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_body_patch.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_border_fill_image_gradient.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_build_element_census.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_byte_patch_identity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_capabilities_surface.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_cell_line_spacing.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_chart_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_check_box_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_column_break.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_comment_node_robustness.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_container_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_contract_docs_sync.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_corpus_read_fidelity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_coverage_ledger.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_coverage_promotion.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_coverage_targets.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_deviations_registry.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_doc_diff.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_context_manager.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_facade_surface.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_formatting.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_merge.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_metadata.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_ops_runtime_boundary.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_save_api.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_document_viewer.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_drop_cap_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_dutmal_compose_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_edit_plan.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_editor_surface_inventory.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_engine_surface_6_0.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_equation_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_equation_converter.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_equation_render.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_error_contract.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_error_messages.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_evalplan_core_primitives.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_existing_document_format_editing.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_field_marks.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_form_field_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_form_fields.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_form_fill_core_primitives.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_form_fit.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_fuzz_catalog_derive_expected.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_fuzz_loop.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_fuzz_regressions.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_gap_closure_tools.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_golden_api.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_header_compat_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_highlight_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_hp_tab_support.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_hwpxlib_corpus_read.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_id_generator_range.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_id_integrity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_image_object_workflow.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_index_mark.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ingest.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_inline_models.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_instid_attribute_case.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_integration_hwpx_compatibility.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_integration_roundtrip.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_layout_cache_scope.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_layout_lint.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_layout_preview.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_library_boundary_closure.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_mail_merge_xlsx.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_markdown_export.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_master_page_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_memo_and_style_editing.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_memo_shape_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_moved_surface_hints.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_mutation_report.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_namespace_handling.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_namespace_hygiene.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_new_features.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_note_authoring_contract.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_fields.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_media.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_notes.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_page.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_refs.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_shapes.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_tables.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_ns_tracking.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_opc_package.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_open_safety_corpus.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_outline_numbering.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_oxml_modularization.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_oxml_parsing.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_packaging_license_metadata.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_packaging_py_typed.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_page_number_control.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_para_pr_dedup.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_paragraph_keep_together.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_paragraph_property_version_switch.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_paragraph_section_management.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_part_hierarchy_read_models.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_polygon_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_product_boundary.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_public_artifact_hygiene.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_public_runtime_boundary.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_read_fidelity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_recover_broken_zip.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_redline_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_redline_verify.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_repair_repack.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_report_utils.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_repr_snapshots.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_return_contract.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_roundtrip_fidelity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_roundtrip_fidelity_corpus.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_row_heights.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_run_choice_atom_authoring.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_save_pipeline.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_section_headers.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_section_properties_real_roundtrip.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_set_paragraph_format_keep.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_shape_geometry_contract.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_shape_open_safety_risk_check.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_skeleton_template_ids.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_split_cell.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_split_merged_cell.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_stable_surface.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_style_resolution.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_tab_definition_version_switch.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_cell_equalize.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_cleanup.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_label.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_patch.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_patch_dryrun.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_patch_m105.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_table_split_merge.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_tables_default_border.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_template_analyzer_enrichment.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_text_extractor_annotations.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_text_extractor_run_choice_atoms.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_title_mark.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_toc_author.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_toc_fidelity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_tracked_overlap_render_smoke.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_validation_severity.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_validator_comment_nodes.py +0 -0
- {python_hwpx-6.3.0 → python_hwpx-6.4.0}/tests/test_version_metadata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python-hwpx
|
|
3
|
-
Version: 6.
|
|
3
|
+
Version: 6.4.0
|
|
4
4
|
Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
|
|
5
5
|
Author: python-hwpx Maintainers
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -68,24 +68,26 @@ Dynamic: license-file
|
|
|
68
68
|
|
|
69
69
|
한컴오피스가 없어도 됩니다. HWPX는 ZIP+XML(OWPML) 포맷이라 순수 파이썬만으로
|
|
70
70
|
읽고, 고치고, 새로 만들 수 있습니다 — Windows·macOS·Linux·CI, 그리고
|
|
71
|
-
**파이썬이 도는 ChatGPT 채팅 안에서도** 그대로 동작합니다. 기존
|
|
72
|
-
|
|
73
|
-
형태로 만들어집니다.
|
|
71
|
+
**파이썬이 도는 ChatGPT 채팅 안에서도** 그대로 동작합니다. 기존 문서의 저장은 요청한 보존 등급과 실제 검증 결과를 영수증으로 남깁니다.
|
|
72
|
+
새 문서의 한컴 호환성은 아래에 날짜·버전을 명시한 코퍼스로 측정합니다.
|
|
74
73
|
|
|
75
74
|
<p align="center">
|
|
76
75
|
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/assets/chatgpt-formfill.png" width="760" alt="일반 ChatGPT 대화에 hwpx 양식을 올려 지도안 작성을 부탁하고, 양식이 유지된 채 채워진 문서를 돌려받는 화면">
|
|
77
76
|
</p>
|
|
78
77
|
<p align="center"><sub>일반 ChatGPT 대화 — 양식 <code>.hwpx</code>를 올리고 말로 부탁하면, 서식을 유지한 채 채워진 문서가 돌아옵니다.</sub></p>
|
|
79
78
|
|
|
80
|
-
**ChatGPT에서 그대로 따라 하기** —
|
|
79
|
+
**ChatGPT에서 그대로 따라 하기** — ChatGPT 채팅의 파이썬 환경은 PyPI 접근이
|
|
80
|
+
막혀 있으므로 [최신 Release](https://github.com/airmang/python-hwpx/releases/latest)에서
|
|
81
|
+
`python_hwpx-*.whl` 하나를 받아 문서와 **함께 올리고** 이렇게 부탁합니다:
|
|
81
82
|
|
|
82
83
|
```text
|
|
84
|
+
첨부한 python_hwpx-*.whl 을 pip 로 설치한 다음(pip install /mnt/data/python_hwpx-*.whl),
|
|
83
85
|
이 .hwpx 파일을 python-hwpx 라이브러리로 열어서 작업해줘.
|
|
84
|
-
(pip install python-hwpx 로 설치하면 돼)
|
|
85
86
|
양식과 서식은 그대로 두고, ○○만 바꿔서 새 파일로 돌려줘.
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
설치부터 결과 파일까지 대화 안에서 끝납니다 — 내 컴퓨터에 파이썬이 없어도 됩니다.
|
|
90
|
+
자세한 절차와 에이전트용 지시문은 [AI 채팅 환경에서 쓰기](docs/ai-assistants.md)에,
|
|
89
91
|
AI 도구가 정확한 API를 배우도록 [llms.txt](https://airmang.github.io/python-hwpx/llms.txt)도 제공합니다.
|
|
90
92
|
|
|
91
93
|
| | 저장소 | 역할 |
|
|
@@ -136,25 +138,32 @@ doc.save_to_path("계획.hwpx")
|
|
|
136
138
|
|
|
137
139
|
```python
|
|
138
140
|
doc = HwpxDocument.open("신청서.hwpx")
|
|
139
|
-
|
|
141
|
+
values = {
|
|
140
142
|
"성명 > right": "홍길동",
|
|
141
143
|
"소속 > right": "플랫폼팀",
|
|
142
|
-
}
|
|
143
|
-
doc.
|
|
144
|
+
}
|
|
145
|
+
result = doc.tables.fill_by_path(values)
|
|
146
|
+
if result["failed_count"] or result["applied_count"] != len(values):
|
|
147
|
+
raise ValueError(result["failed"])
|
|
148
|
+
report = doc.save_to_path("신청서-작성완료.hwpx", mode="patch", fallback="error", return_report=True)
|
|
144
149
|
```
|
|
145
150
|
|
|
146
|
-
|
|
151
|
+
중복·누락 라벨은 실패로 확인하고 저장을 중단합니다. patch는 미수정 ZIP 파트의
|
|
152
|
+
바이트 보존을 요구합니다. 수정 파트 내부의 보존과 시각적 배치는 별도 확인이
|
|
153
|
+
필요합니다. 출력 재개봉과 내용 확인까지 포함한 경로는 [안전한 쓰기 계약](docs/safe-write-contract.md)을 따르세요.
|
|
147
154
|
|
|
148
155
|
### 저장에는 영수증이 따라옵니다
|
|
149
156
|
|
|
150
157
|
```python
|
|
151
158
|
report = doc.save_to_path("결과.hwpx", return_report=True)
|
|
152
|
-
print(report.actual_mode) # "patch" —
|
|
159
|
+
print(report.actual_mode) # "patch" 또는 "rebuild" — 실제 저장 등급
|
|
153
160
|
print(report.preservation.untouched_part_payloads.to_dict())
|
|
154
161
|
# {"verified": 17, "changed": 0}
|
|
155
162
|
```
|
|
156
163
|
|
|
157
|
-
|
|
164
|
+
`mode="patch", fallback="error"`는 보존 등급 미달 시 출력하지 않습니다.
|
|
165
|
+
기본 `auto`는 달성 가능한 등급을 선택합니다. `report.ok`는 요청 반영이나 시각
|
|
166
|
+
검증 완료를 뜻하지 않습니다.
|
|
158
167
|
전체 규칙: [안전한 쓰기 계약](docs/safe-write-contract.md).
|
|
159
168
|
|
|
160
169
|
*예제 중 **독립 실행 예제** 표시가 없는 블록은 여러분의 기존 문서를 입력으로
|
|
@@ -190,17 +199,20 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
190
199
|
|
|
191
200
|
| 환경 | 무엇을 하면 되나 |
|
|
192
201
|
|---|---|
|
|
193
|
-
| 일반 ChatGPT 대화 | `.hwpx
|
|
202
|
+
| 일반 ChatGPT 대화 | `.hwpx`와 [Release wheel](https://github.com/airmang/python-hwpx/releases/latest)을 함께 올리고 오프라인 설치 후 편집해 되받기 — [안내](docs/ai-assistants.md) |
|
|
194
203
|
| 로컬·서버 파이썬 | `pip install python-hwpx` — 스크립트·배치·CI |
|
|
195
204
|
| 파이썬 자동화 (MCP 없이) | `pip install python-hwpx-automation` — 저작·양식 채움·검증 워크플로를 그냥 파이썬으로 |
|
|
196
205
|
| ChatGPT MCP 앱 | [`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)의 MCP adapter를 커넥터로 등록 |
|
|
197
206
|
| Codex 마켓플레이스 플러그인 | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) 설치 |
|
|
198
207
|
| Claude Code · Hermes · OpenClaw | 같은 MCP 서버를 각 클라이언트에 등록 ([`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)) |
|
|
199
208
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
209
|
+
ChatGPT 채팅의 파이썬 환경은 현재 PyPI 접근이 막혀 있습니다. 매 릴리스의
|
|
210
|
+
GitHub Release에 첨부되는 `py3-none-any` wheel을 문서와 함께 올리면 오프라인
|
|
211
|
+
설치로 같은 여정이 됩니다. wheel의 유일한 의존성은 `lxml`이며, 실행 환경에
|
|
212
|
+
없으면 lxml wheel도 함께 올립니다. 같은 wheel을 GitHub Actions artifact
|
|
213
|
+
`python-hwpx-wheel`로도 유지하므로, artifact를 가져올 수 있는 에이전트는 첨부
|
|
214
|
+
없이 설치할 수 있습니다(실험 경로, [안내](docs/ai-assistants.md)). 파이썬 실행
|
|
215
|
+
가능 여부와 업로드 경로는 플랜·설정에 따라 다릅니다.
|
|
204
216
|
|
|
205
217
|
## 비교
|
|
206
218
|
|
|
@@ -17,24 +17,26 @@
|
|
|
17
17
|
|
|
18
18
|
한컴오피스가 없어도 됩니다. HWPX는 ZIP+XML(OWPML) 포맷이라 순수 파이썬만으로
|
|
19
19
|
읽고, 고치고, 새로 만들 수 있습니다 — Windows·macOS·Linux·CI, 그리고
|
|
20
|
-
**파이썬이 도는 ChatGPT 채팅 안에서도** 그대로 동작합니다. 기존
|
|
21
|
-
|
|
22
|
-
형태로 만들어집니다.
|
|
20
|
+
**파이썬이 도는 ChatGPT 채팅 안에서도** 그대로 동작합니다. 기존 문서의 저장은 요청한 보존 등급과 실제 검증 결과를 영수증으로 남깁니다.
|
|
21
|
+
새 문서의 한컴 호환성은 아래에 날짜·버전을 명시한 코퍼스로 측정합니다.
|
|
23
22
|
|
|
24
23
|
<p align="center">
|
|
25
24
|
<img src="https://raw.githubusercontent.com/airmang/python-hwpx/main/docs/assets/chatgpt-formfill.png" width="760" alt="일반 ChatGPT 대화에 hwpx 양식을 올려 지도안 작성을 부탁하고, 양식이 유지된 채 채워진 문서를 돌려받는 화면">
|
|
26
25
|
</p>
|
|
27
26
|
<p align="center"><sub>일반 ChatGPT 대화 — 양식 <code>.hwpx</code>를 올리고 말로 부탁하면, 서식을 유지한 채 채워진 문서가 돌아옵니다.</sub></p>
|
|
28
27
|
|
|
29
|
-
**ChatGPT에서 그대로 따라 하기** —
|
|
28
|
+
**ChatGPT에서 그대로 따라 하기** — ChatGPT 채팅의 파이썬 환경은 PyPI 접근이
|
|
29
|
+
막혀 있으므로 [최신 Release](https://github.com/airmang/python-hwpx/releases/latest)에서
|
|
30
|
+
`python_hwpx-*.whl` 하나를 받아 문서와 **함께 올리고** 이렇게 부탁합니다:
|
|
30
31
|
|
|
31
32
|
```text
|
|
33
|
+
첨부한 python_hwpx-*.whl 을 pip 로 설치한 다음(pip install /mnt/data/python_hwpx-*.whl),
|
|
32
34
|
이 .hwpx 파일을 python-hwpx 라이브러리로 열어서 작업해줘.
|
|
33
|
-
(pip install python-hwpx 로 설치하면 돼)
|
|
34
35
|
양식과 서식은 그대로 두고, ○○만 바꿔서 새 파일로 돌려줘.
|
|
35
36
|
```
|
|
36
37
|
|
|
37
38
|
설치부터 결과 파일까지 대화 안에서 끝납니다 — 내 컴퓨터에 파이썬이 없어도 됩니다.
|
|
39
|
+
자세한 절차와 에이전트용 지시문은 [AI 채팅 환경에서 쓰기](docs/ai-assistants.md)에,
|
|
38
40
|
AI 도구가 정확한 API를 배우도록 [llms.txt](https://airmang.github.io/python-hwpx/llms.txt)도 제공합니다.
|
|
39
41
|
|
|
40
42
|
| | 저장소 | 역할 |
|
|
@@ -85,25 +87,32 @@ doc.save_to_path("계획.hwpx")
|
|
|
85
87
|
|
|
86
88
|
```python
|
|
87
89
|
doc = HwpxDocument.open("신청서.hwpx")
|
|
88
|
-
|
|
90
|
+
values = {
|
|
89
91
|
"성명 > right": "홍길동",
|
|
90
92
|
"소속 > right": "플랫폼팀",
|
|
91
|
-
}
|
|
92
|
-
doc.
|
|
93
|
+
}
|
|
94
|
+
result = doc.tables.fill_by_path(values)
|
|
95
|
+
if result["failed_count"] or result["applied_count"] != len(values):
|
|
96
|
+
raise ValueError(result["failed"])
|
|
97
|
+
report = doc.save_to_path("신청서-작성완료.hwpx", mode="patch", fallback="error", return_report=True)
|
|
93
98
|
```
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
중복·누락 라벨은 실패로 확인하고 저장을 중단합니다. patch는 미수정 ZIP 파트의
|
|
101
|
+
바이트 보존을 요구합니다. 수정 파트 내부의 보존과 시각적 배치는 별도 확인이
|
|
102
|
+
필요합니다. 출력 재개봉과 내용 확인까지 포함한 경로는 [안전한 쓰기 계약](docs/safe-write-contract.md)을 따르세요.
|
|
96
103
|
|
|
97
104
|
### 저장에는 영수증이 따라옵니다
|
|
98
105
|
|
|
99
106
|
```python
|
|
100
107
|
report = doc.save_to_path("결과.hwpx", return_report=True)
|
|
101
|
-
print(report.actual_mode) # "patch" —
|
|
108
|
+
print(report.actual_mode) # "patch" 또는 "rebuild" — 실제 저장 등급
|
|
102
109
|
print(report.preservation.untouched_part_payloads.to_dict())
|
|
103
110
|
# {"verified": 17, "changed": 0}
|
|
104
111
|
```
|
|
105
112
|
|
|
106
|
-
|
|
113
|
+
`mode="patch", fallback="error"`는 보존 등급 미달 시 출력하지 않습니다.
|
|
114
|
+
기본 `auto`는 달성 가능한 등급을 선택합니다. `report.ok`는 요청 반영이나 시각
|
|
115
|
+
검증 완료를 뜻하지 않습니다.
|
|
107
116
|
전체 규칙: [안전한 쓰기 계약](docs/safe-write-contract.md).
|
|
108
117
|
|
|
109
118
|
*예제 중 **독립 실행 예제** 표시가 없는 블록은 여러분의 기존 문서를 입력으로
|
|
@@ -139,17 +148,20 @@ print(report.preservation.untouched_part_payloads.to_dict())
|
|
|
139
148
|
|
|
140
149
|
| 환경 | 무엇을 하면 되나 |
|
|
141
150
|
|---|---|
|
|
142
|
-
| 일반 ChatGPT 대화 | `.hwpx
|
|
151
|
+
| 일반 ChatGPT 대화 | `.hwpx`와 [Release wheel](https://github.com/airmang/python-hwpx/releases/latest)을 함께 올리고 오프라인 설치 후 편집해 되받기 — [안내](docs/ai-assistants.md) |
|
|
143
152
|
| 로컬·서버 파이썬 | `pip install python-hwpx` — 스크립트·배치·CI |
|
|
144
153
|
| 파이썬 자동화 (MCP 없이) | `pip install python-hwpx-automation` — 저작·양식 채움·검증 워크플로를 그냥 파이썬으로 |
|
|
145
154
|
| ChatGPT MCP 앱 | [`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)의 MCP adapter를 커넥터로 등록 |
|
|
146
155
|
| Codex 마켓플레이스 플러그인 | [`hwpx-plugins`](https://github.com/airmang/hwpx-plugins) 설치 |
|
|
147
156
|
| Claude Code · Hermes · OpenClaw | 같은 MCP 서버를 각 클라이언트에 등록 ([`python-hwpx-automation`](https://github.com/airmang/python-hwpx-automation)) |
|
|
148
157
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
ChatGPT 채팅의 파이썬 환경은 현재 PyPI 접근이 막혀 있습니다. 매 릴리스의
|
|
159
|
+
GitHub Release에 첨부되는 `py3-none-any` wheel을 문서와 함께 올리면 오프라인
|
|
160
|
+
설치로 같은 여정이 됩니다. wheel의 유일한 의존성은 `lxml`이며, 실행 환경에
|
|
161
|
+
없으면 lxml wheel도 함께 올립니다. 같은 wheel을 GitHub Actions artifact
|
|
162
|
+
`python-hwpx-wheel`로도 유지하므로, artifact를 가져올 수 있는 에이전트는 첨부
|
|
163
|
+
없이 설치할 수 있습니다(실험 경로, [안내](docs/ai-assistants.md)). 파이썬 실행
|
|
164
|
+
가능 여부와 업로드 경로는 플랜·설정에 따라 다릅니다.
|
|
153
165
|
|
|
154
166
|
## 비교
|
|
155
167
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "python-hwpx"
|
|
7
|
-
version = "6.
|
|
7
|
+
version = "6.4.0"
|
|
8
8
|
description = "한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리"
|
|
9
9
|
readme = { file = "README.md", content-type = "text/markdown" }
|
|
10
10
|
license = "Apache-2.0"
|
|
@@ -217,6 +217,7 @@ files = [
|
|
|
217
217
|
"src/hwpx/oxml/numbering.py",
|
|
218
218
|
"src/hwpx/oxml/objects.py",
|
|
219
219
|
"src/hwpx/opc/package.py",
|
|
220
|
+
"src/hwpx/opc/xml_utils.py",
|
|
220
221
|
"src/hwpx/opc/relationships.py",
|
|
221
222
|
"src/hwpx/oxml/paragraph.py",
|
|
222
223
|
"src/hwpx/oxml/run.py",
|
|
@@ -383,6 +384,7 @@ include = [
|
|
|
383
384
|
"src/hwpx/oxml/numbering.py",
|
|
384
385
|
"src/hwpx/oxml/objects.py",
|
|
385
386
|
"src/hwpx/opc/package.py",
|
|
387
|
+
"src/hwpx/opc/xml_utils.py",
|
|
386
388
|
"src/hwpx/opc/relationships.py",
|
|
387
389
|
"src/hwpx/oxml/paragraph.py",
|
|
388
390
|
"src/hwpx/oxml/run.py",
|
|
@@ -689,20 +689,15 @@ def _insert_form_field_text_run(
|
|
|
689
689
|
match: dict[str, Any],
|
|
690
690
|
value: str,
|
|
691
691
|
) -> None:
|
|
692
|
-
|
|
692
|
+
# A field may begin and end within one run. A run index is not a
|
|
693
|
+
# paragraph-child insertion point, and inserting before the end run can
|
|
694
|
+
# put the value before fieldBegin. Insert the text immediately after the
|
|
695
|
+
# actual begin control, retaining its surrounding run and character style.
|
|
693
696
|
runs: list[Any] = match["_runs"]
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
begin_run = runs[begin_run_index]
|
|
697
|
-
char_ref = begin_run.get("charPrIDRef") or paragraph.char_pr_id_ref or "0"
|
|
698
|
-
run = paragraph.element.makeelement(f"{_HP}run", {"charPrIDRef": str(char_ref)})
|
|
699
|
-
text_node = run.makeelement(f"{_HP}t", {})
|
|
697
|
+
begin_run = runs[int(match["_begin_run_index"])]
|
|
698
|
+
text_node = begin_run.makeelement(f"{_HP}t", {})
|
|
700
699
|
text_node.text = _sanitize_field_text(value)
|
|
701
|
-
|
|
702
|
-
if end_run_index is None:
|
|
703
|
-
paragraph.element.insert(begin_run_index + 1, run)
|
|
704
|
-
else:
|
|
705
|
-
paragraph.element.insert(int(end_run_index), run)
|
|
700
|
+
begin_run.insert(int(match["_begin_child_index"]) + 1, text_node)
|
|
706
701
|
|
|
707
702
|
|
|
708
703
|
def fill_form_field(
|
|
@@ -74,7 +74,7 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
|
74
74
|
"area": "paragraph-table-authoring",
|
|
75
75
|
"namespace": None,
|
|
76
76
|
"matrix_row": "문단·표 저작/편집",
|
|
77
|
-
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
77
|
+
"entry_points": ("hwpx.document:HwpxDocument", "hwpx.model:Table"),
|
|
78
78
|
"authoring_methods": ("add_heading", "add_paragraph", "add_section"),
|
|
79
79
|
},
|
|
80
80
|
{
|
|
@@ -116,7 +116,7 @@ _CAPABILITY_AREAS: tuple[dict[str, Any], ...] = (
|
|
|
116
116
|
"area": "shape-authoring",
|
|
117
117
|
"namespace": "doc.shapes",
|
|
118
118
|
"matrix_row": "도형 저작(선·사각형·타원)",
|
|
119
|
-
"entry_points": ("hwpx.document:HwpxDocument",),
|
|
119
|
+
"entry_points": ("hwpx.document:HwpxDocument", "hwpx.model:Shape"),
|
|
120
120
|
"authoring_methods": ("add_line", "add_rectangle", "add_ellipse"),
|
|
121
121
|
},
|
|
122
122
|
{
|
|
@@ -14,12 +14,12 @@ stable 편집 표면의 계약을 한 곳에 모았다. 아래 표의 실패 모
|
|
|
14
14
|
| 호출 | 반환 | 대표 실패 모드 | 다시 실행하면 |
|
|
15
15
|
|---|---|---|---|
|
|
16
16
|
| `add_paragraph(text)` | `HwpxOxmlParagraph` | 사실상 없음 | 문단이 하나 더 추가된다(append, 비멱등) |
|
|
17
|
-
| `
|
|
17
|
+
| `paragraph.remove()` | `None` | 섹션의 마지막 단락 삭제 시 `ValueError` | 이미 제거된 문단이면 조용히 무시된다(무해) |
|
|
18
18
|
| `add_table(rows, cols)` | `HwpxOxmlTable` | 사실상 없음 | 표가 하나 더 추가된다(비멱등) |
|
|
19
19
|
| `table.set_cell_text(r, c, text)` | `FitResult \| None` | 범위 밖 좌표는 `IndexError` (`exceed table bounds`) | 같은 값이면 결과 동일(수렴) |
|
|
20
|
-
| `
|
|
21
|
-
| `
|
|
22
|
-
| `add_footnote(text, paragraph)` | `HwpxOxmlNote` | 사실상 없음 | 각주가 하나 더 붙는다(비멱등) |
|
|
20
|
+
| `document.text.replace(search, repl)` | `int` (치환 수) | 빈 `search`는 `ValueError` | 치환할 것이 없으면 `0` — 1회차 후 수렴 |
|
|
21
|
+
| `document.notes.add_memo(..., anchor=p)` | `Memo` (`paragraph`, `field_id` 속성) | 아래 캐비앗 참고 | 메모가 하나 더 붙는다(비멱등) |
|
|
22
|
+
| `document.notes.add_footnote(text, paragraph)` | `HwpxOxmlNote` | 사실상 없음 | 각주가 하나 더 붙는다(비멱등) |
|
|
23
23
|
|
|
24
24
|
"사실상 없음"은 정상 인자에서 실패 경로가 없다는 뜻이다 — 타입이 어긋난
|
|
25
25
|
인자는 여느 파이썬 API처럼 `TypeError` 계열로 즉시 드러난다.
|
|
@@ -35,19 +35,25 @@ print(report.actual_mode)
|
|
|
35
35
|
안전 검증을 통과한 결과만 대상 경로를 교체한다.
|
|
36
36
|
- 요청한 보존 등급(`mode="patch"` 등)을 지킬 수 없으면
|
|
37
37
|
`PreservationDowngradeError`로 실패하고 **아무것도 쓰지 않는다**
|
|
38
|
-
(
|
|
38
|
+
(`mode="patch", fallback="error"`). 기본 `auto`는 달성 가능한 등급을 선택하며,
|
|
39
|
+
명시한 `fallback="rebuild"`는 강등을 허용한다.
|
|
39
40
|
- `return_report=True`는 `MutationReport`를 돌려준다 — 실제 저장 모드,
|
|
40
41
|
손대지 않은 파트의 바이트 보존 검증 결과까지. 스키마와 전체 규칙은
|
|
41
42
|
[안전한 쓰기 계약](safe-write-contract.md).
|
|
43
|
+
- `report.ok`는 요청 내용 반영을 검사하지 않는다. 요청 건수와 출력 값을 별도로
|
|
44
|
+
확인하고 `verification.visual="not_performed"`를 시각 통과로 읽지 않는다.
|
|
42
45
|
- **직렬화는 결정론적이다**: 같은 문서 상태에서 `to_bytes()`를 두 번 부르면
|
|
43
46
|
바이트가 동일하다(실측). diff·해시 기반 파이프라인에 안전하다.
|
|
44
47
|
|
|
45
48
|
## 알아둘 캐비앗 (정직 고지)
|
|
46
49
|
|
|
47
|
-
- `
|
|
50
|
+
- `document.notes.add_memo(memo_shape_id_ref=...)`는 참조가 실재하는 메모
|
|
48
51
|
모양인지 **검증하지 않고 조용히 수용한다**. 존재하지 않는 ID를 넣으면
|
|
49
|
-
저장은 되지만 편집기 표시가 어긋날 수 있다. `document.memo_shapes`로
|
|
52
|
+
저장은 되지만 편집기 표시가 어긋날 수 있다. `document.styles.memo_shapes`로
|
|
50
53
|
실재 ID를 확인하고 쓰는 것을 권장한다.
|
|
54
|
+
- `document.text.replace`는 본문 문단의 개별 런 안에서 치환한다. 여러 런에
|
|
55
|
+
걸친 검색어와 중첩 셀 문단은 이 경로의 대상이 아니다. `0`을 반환하면 저장
|
|
56
|
+
성공 여부와 관계없이 요청이 반영되지 않은 것이다.
|
|
51
57
|
- `add_*` 계열은 전부 append 의미론이다. "없으면 추가"가 필요하면 먼저
|
|
52
58
|
{doc}`recipes-traversal`의 순회로 존재 여부를 확인하라.
|
|
53
59
|
- 편집은 저장 전까지 메모리에만 있다. 저장 경로가 곧 커밋이다.
|
|
@@ -79,34 +79,41 @@ print("셀 수:", len(cells))
|
|
|
79
79
|
|
|
80
80
|
## 런 단위로 서식과 함께 순회하기
|
|
81
81
|
|
|
82
|
-
굵기·색 같은 서식은 런(run)에 붙는다.
|
|
82
|
+
굵기·색 같은 서식은 런(run)에 붙는다. 본문 문단의 런을 순회하거나, 특정
|
|
83
83
|
서식의 런만 골라낼 수 있다.
|
|
84
84
|
|
|
85
85
|
```python
|
|
86
86
|
from hwpx import HwpxDocument
|
|
87
87
|
|
|
88
88
|
document = HwpxDocument.open("input.hwpx")
|
|
89
|
-
for run in document.
|
|
89
|
+
for run in document.text.runs():
|
|
90
90
|
print(repr(run.text), "charPr:", run.char_pr_id_ref)
|
|
91
91
|
|
|
92
|
-
red_runs = document.
|
|
92
|
+
red_runs = document.text.find_runs(text_color="#FF0000")
|
|
93
93
|
print("빨간 런:", len(red_runs))
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
## 병합 셀의 라벨 찾기
|
|
97
|
+
|
|
98
|
+
라벨 기반 표 탐색에는 `document.tables.find_cell_by_label(label)`을 쓴다.
|
|
99
|
+
병합 셀 하나는 라벨 한 건으로 세며, 오른쪽/아래쪽 이동은 그 병합 범위 밖의
|
|
100
|
+
인접 셀로 간다. 서로 다른 셀에 같은 라벨이 있으면 여러 결과를 돌려준다.
|
|
101
|
+
논리 좌표 여러 개가 같은 병합 셀을 가리키는 경우와 실제 중복 라벨은 구분한다.
|
|
102
|
+
|
|
96
103
|
## 메모와 누름틀(필드) 나열하기
|
|
97
104
|
|
|
98
105
|
```python
|
|
99
106
|
from hwpx import HwpxDocument
|
|
100
107
|
|
|
101
108
|
document = HwpxDocument.open("input.hwpx")
|
|
102
|
-
for memo in document.memos:
|
|
109
|
+
for memo in document.notes.memos:
|
|
103
110
|
print("메모:", memo.id)
|
|
104
111
|
|
|
105
|
-
for field in document.
|
|
112
|
+
for field in document.fields.all:
|
|
106
113
|
print("누름틀:", field)
|
|
107
114
|
```
|
|
108
115
|
|
|
109
|
-
문서에 해당 요소가 없으면 빈
|
|
116
|
+
문서에 해당 요소가 없으면 빈 컬렉션이 나온다 — 예외가 아니다.
|
|
110
117
|
|
|
111
118
|
## 다음 단계
|
|
112
119
|
|
|
@@ -21,12 +21,12 @@
|
|
|
21
21
|
|
|
22
22
|
| 능력 영역 | 상태 | 증거 |
|
|
23
23
|
|---|---|---|
|
|
24
|
-
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | corpus-metrics「오픈 수용률」476/476, 「저작 품질 게이트」실저작 58/58, 「렌더 검증」416건. **6.5+**: `paragraph.add_run(text, expand_special_characters=True)`가 `hp:lineBreak`(줄바꿈)·`hp:nbSpace`(비분리공백)·`hp:fwSpace`(전각공백)를 저작한다 — 실코퍼스 3파일(`error__20230818__test.hwpx`/`error__20251107__test.hwpx`/`error__20250808__...hwpx`) 리버스로 셋 다 **단일 hp:t 안의 mixed content**임을 확인(`hp:tab`이 쓰는 "hp:run 형제" 형태와 다름 — 스키마는 둘 다 허용하나 실 산출물은 이 셋에 대해 전자만 씀). 같은 리버스 과정에서 텍스트 추출기(`TextExtractor`)가 이 세 원자(+`hp:hyphen`)를 `hp:t` 중첩 위치에서 못 읽던 결함도 함께 수리 — 수리 전엔 원자가 조용히 사라지며 앞뒤 텍스트가 구분자 없이 붙어 읽혔다(줄바꿈이 있었다는 사실 자체가 유실). 실한컴 렌더 검증: `docs/openrate/report-v9.json` authored-inlineatoms 스트라텀(3원자의 0공백 순서조합 15종 전량), macOS Hancom GUI 오라클 15/15 render_checked·0 render_failed(2026-08-07 측정). **6.12 트레인㊸ 갭④**: `doc.styles.apply_paragraph_format(column_break=...)`가 `hp:p`의 `columnBreak` 속성(문단 인스턴스별 강제 단 나눔)을 저작한다 — `page_break_before`가 다루는 `hh:breakSetting`(paraPr 공유 스타일)과는 다른 메커니즘이라 대상 paraPr을 새로 안 만들고 문단에 직접 적용된다. 실코퍼스 67파일 전수(14266문단) 측정: 자매 속성 `pageBreak`는 73건 실사용 확인(항상 "0"/"1" 리터럴, "true"/"false" 0건)되지만 `columnBreak`는 0건 — 그래도 같은 요소·같은 리터럴 규약을 재사용하는 것뿐이라 구조적 추측 위험은 없다. 실한컴 렌더 검증: `docs/openrate/report-v16.json` authored-column-break 스트라텀(5문단 문서에서 대상 문단 위치를 0~4로 로테이션, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-10 측정). 옛 5.x 표면(`doc.set_paragraph_format`)에는 의도적으로 안 얹었다 — 그 shim은 5.x 시그니처 그대로 얼려두는 게 계약이라(7.0 제거 예정), 신규 옵션은 `doc.styles.apply_paragraph_format`에만 산다 |
|
|
24
|
+
| 문단·표 저작/편집 | Parse·Preserve·Edit·Create·Render-verified | `Table.set_cell_borders`는 네 변 색/종류만 수정하며 기존 선 두께·배경·대각선과 공유 스타일을 보존한다. 전체 스타일 교체용 `set_cell_border_fill`과 구분한다. corpus-metrics「오픈 수용률」476/476, 「저작 품질 게이트」실저작 58/58, 「렌더 검증」416건. **6.5+**: `paragraph.add_run(text, expand_special_characters=True)`가 `hp:lineBreak`(줄바꿈)·`hp:nbSpace`(비분리공백)·`hp:fwSpace`(전각공백)를 저작한다 — 실코퍼스 3파일(`error__20230818__test.hwpx`/`error__20251107__test.hwpx`/`error__20250808__...hwpx`) 리버스로 셋 다 **단일 hp:t 안의 mixed content**임을 확인(`hp:tab`이 쓰는 "hp:run 형제" 형태와 다름 — 스키마는 둘 다 허용하나 실 산출물은 이 셋에 대해 전자만 씀). 같은 리버스 과정에서 텍스트 추출기(`TextExtractor`)가 이 세 원자(+`hp:hyphen`)를 `hp:t` 중첩 위치에서 못 읽던 결함도 함께 수리 — 수리 전엔 원자가 조용히 사라지며 앞뒤 텍스트가 구분자 없이 붙어 읽혔다(줄바꿈이 있었다는 사실 자체가 유실). 실한컴 렌더 검증: `docs/openrate/report-v9.json` authored-inlineatoms 스트라텀(3원자의 0공백 순서조합 15종 전량), macOS Hancom GUI 오라클 15/15 render_checked·0 render_failed(2026-08-07 측정). **6.12 트레인㊸ 갭④**: `doc.styles.apply_paragraph_format(column_break=...)`가 `hp:p`의 `columnBreak` 속성(문단 인스턴스별 강제 단 나눔)을 저작한다 — `page_break_before`가 다루는 `hh:breakSetting`(paraPr 공유 스타일)과는 다른 메커니즘이라 대상 paraPr을 새로 안 만들고 문단에 직접 적용된다. 실코퍼스 67파일 전수(14266문단) 측정: 자매 속성 `pageBreak`는 73건 실사용 확인(항상 "0"/"1" 리터럴, "true"/"false" 0건)되지만 `columnBreak`는 0건 — 그래도 같은 요소·같은 리터럴 규약을 재사용하는 것뿐이라 구조적 추측 위험은 없다. 실한컴 렌더 검증: `docs/openrate/report-v16.json` authored-column-break 스트라텀(5문단 문서에서 대상 문단 위치를 0~4로 로테이션, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-10 측정). 옛 5.x 표면(`doc.set_paragraph_format`)에는 의도적으로 안 얹었다 — 그 shim은 5.x 시그니처 그대로 얼려두는 게 계약이라(7.0 제거 예정), 신규 옵션은 `doc.styles.apply_paragraph_format`에만 산다 |
|
|
25
25
|
| 표 구조 변경(행·열·표 삭제/삽입, 열 오토핏) | Preserve·Edit·Render-verified | `hwpx.table_patch.apply_table_ops`; corpus-metrics「바이트 보존」497/497(patch 경로). 실한컴 렌더 검증: `docs/openrate/report-v14.json` authored-tablestructure 스트라텀(delete_column/delete_row/insert_row_by_clone/set_column_widths/autofit_columns 각 1건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정). **원장(coverage-ledger) 라우팅은 없음** — `apply_table_ops`가 건드리는 `hp:tbl`/`tr`/`tc`/`cellAddr`/`cellSpan`/`cellSz`/`cellMargin`은 전부 "문단·표 저작/편집"(대형 혼합 영역)에 이미 등록돼 있어, 여기 추가하면 이 스트라텀이 실제로 검증한 적 없는 이웃 요소까지 openrate 근거를 얻는 위양성이 생긴다(v8 polygon/arc 전례와 동일 판단) — 등급 갱신은 이 산문과 report-v14.json 직접 인용으로만. **6.12 트레인㊸ 갭⑤**가 `split_table`(표 나누기)·`merge_table`(표 붙이기) 두 op를 추가했다 — 스키마에 표 나누기/붙이기 전용 어휘는 없다(`hp:tbl`/`TableType`은 `pageBreak`/`repeatHeader`/`rowCnt`/`colCnt`뿐, `ParaList XML schema.xml:2008`), 순수 구조 편집이라 `_insert_block_by_clone`과 같은 fail-closed 원칙(병합 셀이 경계를 걸치면 거부)을 그대로 따른다. `split_table`은 `split_row` 물리 행 인덱스에서 표를 둘로 나눠(윗칸은 원래 표 id·아랫칸은 새 래핑 문단+새 표 id) 병합 셀이 경계를 걸치면 거부한다(실코퍼스 `public_official_table.hwpx`로 clean·crossing 양쪽 검증). `merge_table`은 그 역연산 — 바로 다음 표와 합치되 `colCnt` 불일치나 두 표 사이 실텍스트 존재 시 거부(내용 무음 소실 방지). **"표 뒤집기"는 이번 트레인에서 의도적으로 보류**: 행/열 순서 반전은 병합 셀·중첩표에서 정의가 흐려지고(예: rowSpan 셀을 반전하면 어느 칸이 내용을 갖는가?) 실코퍼스에 근거로 삼을 예시가 전혀 없다(스키마조차 이런 연산의 존재를 암시하지 않는다) — curve·connectLine과 같은 원칙으로 근거 없는 알고리즘을 추측하지 않는다. 실한컴 렌더 검증: `docs/openrate/report-v16.json` authored-table-split-merge 스트라텀(분할 3종[3/4/5행 표를 각기 다른 위치에서 분할]·병합 1건[2+2행 표 결합]·분할 후 재병합 왕복 1건, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-10 측정, 기존 v14의 15/15은 split/merge를 겨냥하지 않았었다) |
|
|
26
26
|
| 표 생성(병합·중첩 포함) | Create·Render-verified | `add_table`·`merge_cells`(피복 셀 제거+cellSpan — 실한컴 병합 의미론과 동형, 3종 병합 렌더 픽셀 확인)·셀 문단 `add_table`(중첩). 기본값은 5.4.0에서 실한컴에 맞췄다: 셀 안여백 510/510/141/141, 기본 표 폭은 본문 폭, 중첩 표는 부모 셀 사용 폭. **남은 갭**: 선 종류는 raw 전용. **6.7+**: `table.label`/`.set_label()`/`.remove_label()`(`hp:label`, Avery형 라벨시트/명패 인쇄 레이아웃) — 사설 코퍼스 75건·436건 리버스(DEV-023, 스키마와 완전 일치), 항상 표의 마지막 자식으로 배치. **Render-verified(6.8, 트레인㉘)**: openrate v11 실한컴 배치(macOS GUI 오라클) 15/15 render-checked·음성대조 3/3 — 관측 2클러스터(2×9 소형시트·1×2 대형 정사각 명패)와 관측 밖 조합(3×6 미관측 그리드·landscape=NARROWLY·partial 속성) 전부 수용, 수용 경계가 관측 분포보다 넓다. **6.13 트레인㊻**: `equalize_column_widths()`/`equalize_row_heights()`(`HwpxOxmlTable`) — 편집기 메뉴 표면 역매핑(트레인㊷·㊺)이 찾은 부분 대응(균등화 "전용" 헬퍼 부재)을 닫는다. 신규 XML 어휘는 전혀 없음: 전자는 기존 `set_column_widths([1]*column_count)`를 전용 이름으로 노출할 뿐이고, 후자는 그 행 대응(같은 `iter_grid`/rowSpan 로직·`_distribute_size` 재사용, 병합 셀은 자신이 덮는 행/열들의 새 크기 합을 받는다 — `cellSz`만 갱신). 실한컴 렌더 검증: `docs/openrate/report-v17.json` authored-cell-equalize 스트라텀(열만/행만/양축/양축+병합 셀 1개 로테이션, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-11 측정) — 기존 `set_column_widths` 자체는 v14의 authored-tablestructure에서 1건 확인됐을 뿐 이 두 신규 편의 메서드 자체는 이번이 첫 독립 검증 |
|
|
27
27
|
| 양식 채움(byte-splice) | Preserve·Edit·Render-verified | `hwpx.patch`·`table_patch`·`body_patch`; corpus-metrics「바이트 보존」497/497. wild 공개 양식의 서식 충실은 2차 실측 후 **산출분 pass 82.1%(23/28)·산출+fail 7.6%(5/66)** — 불가능 타깃은 typed 거부 35건(전수 감별 과잉거부 0). 잔존은 페이지 리플 5건(4건 typed 경고 동반·완전 무음 1건, 「구조결함 2차 실측」절), 표 shape 부류는 측정 인공물로 판명·판정기 교정. **6.11 사이클(트레인㊳ 판정, ㊵ 이후 정리 트레인 생성)**: 이 영역 자체의 render-verified 갭(v14 소탕 대상에서 빠뜨림으로 판정)을 v15로 메움 — `docs/openrate/report-v15.json` authored-formfill 스트라텀(합성 2열 양식 → `table_patch.fill_cells`로 빈 값 셀 byte-splice 채움, 5건), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정). **원장 라우팅은 없음** — `fill_cells`가 건드리는 `hp:t`는 기존 요소 텍스트 치환일 뿐 새 요소를 안 만든다(find-replace·document-merge와 같은 이유) — 등급 갱신은 이 산문과 report-v15.json 직접 인용으로만 |
|
|
28
28
|
| 편집 계획 실행(edit plan) | Preserve·Edit·Render-verified | `hwpx.plan.apply_edit_plan`(experimental, 5.6.0+): 바이트-스플라이스 op 7종을 계획 1파일로 합성 — 정적 선검증→전 체인 인메모리 실행→최종 open-safety 검증→단 1회 원자 쓰기. 중간 실패 시 output·source 바이트 불변(테스트가 바이트 비교로 증명), step별+원본→최종 `hwpx.mutation-report/v1` 실측 사영. 실한컴 렌더 검증: `docs/openrate/report-v14.json` authored-editplan 스트라텀(paragraph_patch/fill_cells/apply_table_ops(중첩)/recolor_runs_by_color/strip_runs_by_color 각 1건 — v3/v4의 옛 `edit-plan` 버킷이 이미 다룬 paragraph_patch+fill_cells 조합 이외의 3개 op가 이번 신규 실증분), macOS Hancom GUI 오라클 5/5 render_checked·0 render_failed(2026-08-08 측정). **원장 라우팅은 없음** — 이 모듈은 기존 op를 체이닝만 할 뿐 대응하는 단일 capabilityArea가 없다(formfield류 저수준 공유 요소와 같은 이유) — 등급 갱신은 이 산문과 report-v14.json 직접 인용으로만 |
|
|
29
|
-
| 도형 저작(선·사각형·타원) | Parse·Preserve·Edit·Create·Render-verified | 전용 `add_line`·`add_rectangle`·`add_ellipse`. 5.0.0에서 기하 네임스페이스(`hc:`)·선 bounding box·`resize()`의 기하 반영을 수정한 뒤 실한컴 12.30.0(build 6446)에서 **개봉 7/7**(선 수평·대각·수직, 사각형, 타원, resize 후 혼합, 그림). 렌더로 사각형 144×72pt+`#CCE5FF`, 타원 100×60pt+`#FFD9CC`, 대각선이 요청 박스를 실제 span, `resize(14400,7200)` 후 새 크기·DASH 반영까지 확인 |
|
|
29
|
+
| 도형 저작(선·사각형·타원) | Parse·Preserve·Edit·Create·Render-verified | 전용 `add_line`·`add_rectangle`·`add_ellipse`. 기존 floating 도형은 `Shape.set_position`으로 오프셋만 편집 가능(글자처럼 취급·위치 요소 없음은 typed 거부; 아래 기존 렌더 실적이 새 이동 경로의 검증을 뜻하지 않음). 5.0.0에서 기하 네임스페이스(`hc:`)·선 bounding box·`resize()`의 기하 반영을 수정한 뒤 실한컴 12.30.0(build 6446)에서 **개봉 7/7**(선 수평·대각·수직, 사각형, 타원, resize 후 혼합, 그림). 렌더로 사각형 144×72pt+`#CCE5FF`, 타원 100×60pt+`#FFD9CC`, 대각선이 요청 박스를 실제 span, `resize(14400,7200)` 후 새 크기·DASH 반영까지 확인 |
|
|
30
30
|
| 저수준 도형·컨트롤 탈출구 | Edit | `add_shape`·`add_control`은 건네받은 요소와 속성만 쓰고 OWPML 필수 하위 요소(`offset`·`orgSz`·`curSz`·`sz`·`pos`·유형별 기하)는 만들지 않는다. 그대로 저장한 문서는 실한컴 12.30.0이 **거부**한다(음성 대조로 확인). 생성 시점의 `UserWarning`은 여전히 하나뿐이지만(휘발성 — 호출 스택에만 남고 파일엔 안 남음), **6.6 트레인㉒**부터 `validate_package`·`validate_editor_open_safety`가 이 위험을 저장 이후에도 검증 결과에 명시적 `warning`으로 남긴다(fail-closed 아님 — `ok`는 그대로 `True`, 정직한 신호만 추가). `hp:container` 부재는 오탐 안 됨(부재는 이 5종을 원래 안 가짐, DEV-016). 도형은 위의 전용 헬퍼를 쓸 것(전용 헬퍼는 경고하지 않는다) |
|
|
31
31
|
| arc·polygon·curve·connectLine | Parse·Preserve·Create(arc·polygon, experimental)·Render-verified(arc·polygon만)·Unsupported-but-preserved(curve·connectLine) | **6.11 트레인㊳ 재발견**: `docs/openrate/report-v8.json`의 authored-polygon/authored-arc 스트라텀(각 15/15 render_checked·0 render_failed)이 애초부터 실재했고 원장(coverage-ledger)에도 `hp:polygon`/`hp:arc`로 이미 정확히 반영돼 있었다 — 하지만 이 행의 등급 문자열에 "Render-verified"가 한 번도 반영되지 않아 편집기 표면 인벤토리가 이 항목을 계속 "미실측"으로 잘못 보고해 왔다(순수 매트릭스 산문 갱신 누락, "차트" 행과 같은 종류의 결함, 이번 트레인에서 함께 바로잡음). curve·connectLine은 아래 서술대로 여전히 정직 보류(저작 API 자체가 없음, Render-verified 대상 아님). 전용 `add_polygon`(6.4+, `doc.shapes`): 실코퍼스(`hwpxlib_corpus/reader_writer__SimplePolygon.hwpx`) 리버스 — 꼭짓점은 `hc:pt`(core 네임스페이스, rect/ellipse와 같은 기하 네임스페이스 계약: 5.0.0 도형 네임스페이스 결함 수리와 동일 축), 자기 bbox 좌상단 원점 로컬 좌표계(정점 목록이 그 자기 `orgSz`와 정확히 일치, 실측). `resize()`는 이미 범용(`pt` 로컬명 스캔)이라 변경 없이 다각형에도 적용된다. 전용 `add_arc`(6.4+, `doc.shapes`): 실코퍼스(`SimpleArc.hwpx`) 리버스 — 스키마상 `hp:arc`는 좌표 3점(`center`/`ax1`/`ax2`)뿐 각도 필드가 아예 없다. 유일한 실 예시는 center가 자기 bbox 모서리에 앉고 ax1이 그 바로 아래, ax2가 그 바로 오른쪽인 사분원 하나뿐이라 그 한 배치만 점 단위로 실측 검증됐고, 나머지 세 모서리는 다른 모든 도형이 이미 쓰는 `hp:flip` 미러링으로 얻는다(새 점 계산 없음 — `corner` 인자, `TOP_LEFT`만 실측·나머지 3개는 유도). `arc_type`(NORMAL/PIE/CHORD)은 스키마 열거값 그대로 통과. **curve는 이번 사이클 정직 보류**: 유일한 실 예시(`SimpleCurve.hwpx`)에서 곡선의 `orgSz`가 꼭짓점(`hp:seg` 앵커점) bbox보다 뚜렷이 크다(폭 16636 vs 앵커점 bbox 15225·높이 21360 vs 20500) — 스플라인이 자기 앵커점 밖으로 부풀어 오르는 실측이며, 한컴의 정확한 곡선 적합 알고리즘 근거 없이 bbox를 추정하면 침묵 오류가 된다. **connectLine도 정직 보류**: 유일한 "Simple" 정본 예시(`SimpleConnectLine.hwpx`)는 자유선이 아니라 `subjectIDRef`로 다른 두 도형(rect·ellipse)을 잇는 "스마트 연결선"이고, `offset`이 음수(부호 없는 32비트로 직렬화된 것을 실측 확인)·`curSz`가 `orgSz`와 다르며·`scaMatrix`가 평행이동까지 얹은 비항등 행렬이라 그 관계식 근거가 없다. 다른 예시(`error__20230818__test.hwpx`, 미부착 `subjectIDRef=0`)는 파일명이 스스로 결함 사례임을 밝히고 `scaMatrix`가 e1=0인 퇴화 행렬이라 정본으로 못 쓴다. `startPt`/`endPt`가 `hp:connectLine`에서는 `hp:` 네임스페이스(`hp:line`의 `hc:startPt`와 다름, 실측 확인)라는 점만 확정 — 도형마다 기하 네임스페이스를 개별 검증해야 한다는 교훈 재확인 |
|
|
32
32
|
| 그룹 개체(컨테이너) | Parse·Create(experimental)·Render-verified | 전용 `add_container`(6.5+, `doc.shapes`): 실코퍼스(`hwpxlib_corpus/reader_writer__SimpleContainer.hwpx` 3부재 + 실문서 2종 71개 컨테이너, 총 74개 표본) 리버스 — 부재는 독립 도형과 완전히 같은 구조(offset/orgSz/curSz/flip/rotationInfo/renderingInfo + 도형별 기하)를 유지하되 `AbstractShapeObjectType` 꼬리(sz/pos/outMargin/shapeComment — 그룹만 가짐)는 없고 `groupLevel="1"`(그룹 자신은 `"0"`), `renderingInfo`의 `transMatrix` 이동성분이 자기 `offset`과 일치, `numberingType="PICTURE"` 전량 관측. `ContainerMember.rect`/`.ellipse`/`.polygon`으로 부재를 그룹 로컬 좌표에 배치 — **`pic`/`arc`/`line`/`connectLine`/중첩 `container` 부재와 이미 배치된 도형의 재그룹은 이번 사이클 범위 밖**(전자는 같은 패턴으로 자연 확장 가능, `pic`은 media 배선이 추가로 필요). **알려진 한계**: `resize()`는 컨테이너 자신의 크기만 갱신하고 부재를 비례 재배치하지 않는다(직속 자식 스캔이 부재 도형 전체를 보지, 그 안의 점 좌표를 안 봄) — 부재별로 개별 `resize()`를 부를 것. 실한컴 렌더 검증: `docs/openrate/report-v9.json` authored-container 스트라텀(2~4부재·rect/ellipse/polygon 슬롯별 회전), macOS Hancom GUI 오라클 15/15 render_checked·0 render_failed(2026-08-07 측정) |
|
|
@@ -386,6 +386,27 @@ def _cell_margin_vertical(cell_element: object) -> tuple[int, int]:
|
|
|
386
386
|
return (0, 0)
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def _effective_cell_margins(cell: object) -> tuple[int, int, int, int]:
|
|
390
|
+
"""Respect explicit table-margin inheritance instead of inactive cell zeros."""
|
|
391
|
+
element = getattr(cell, "element", None)
|
|
392
|
+
if element is None:
|
|
393
|
+
return (0, 0, 0, 0)
|
|
394
|
+
if element.get("hasMargin") in {"0", "false", "False"}:
|
|
395
|
+
table_element = getattr(getattr(cell, "table", None), "element", None)
|
|
396
|
+
if table_element is not None:
|
|
397
|
+
for child in table_element:
|
|
398
|
+
if _local_name(child.tag) == "inMargin":
|
|
399
|
+
return (
|
|
400
|
+
int(child.get("left", "0") or 0),
|
|
401
|
+
int(child.get("right", "0") or 0),
|
|
402
|
+
int(child.get("top", "0") or 0),
|
|
403
|
+
int(child.get("bottom", "0") or 0),
|
|
404
|
+
)
|
|
405
|
+
left, right = _cell_margin(element)
|
|
406
|
+
top, bottom = _cell_margin_vertical(element)
|
|
407
|
+
return left, right, top, bottom
|
|
408
|
+
|
|
409
|
+
|
|
389
410
|
def _first_para_line_spacing_ratio(cell: object, document: object) -> float | None:
|
|
390
411
|
"""Per-line em multiple from the cell's first paragraph line spacing (PERCENT).
|
|
391
412
|
|
|
@@ -478,7 +499,7 @@ def resolve_slot_metrics(
|
|
|
478
499
|
|
|
479
500
|
raw_width = float(getattr(cell, "width", 0) or 0)
|
|
480
501
|
element = getattr(cell, "element", None)
|
|
481
|
-
left, right
|
|
502
|
+
left, right, top, bottom = _effective_cell_margins(cell)
|
|
482
503
|
inner = max(raw_width - left - right, 0.0) * safety
|
|
483
504
|
inline_width, inline_count = (
|
|
484
505
|
_inline_object_width(element) if element is not None else (0.0, 0)
|
|
@@ -495,7 +516,6 @@ def resolve_slot_metrics(
|
|
|
495
516
|
row_span = int(getattr(cell, "span", (1, 1))[0])
|
|
496
517
|
except Exception: # pragma: no cover - defensive
|
|
497
518
|
row_span = 1
|
|
498
|
-
top, bottom = _cell_margin_vertical(element) if element is not None else (0, 0)
|
|
499
519
|
inner_h = max(raw_height - top - bottom, 0.0) * safety if raw_height > 0 else 0.0
|
|
500
520
|
# A cell authored shorter than one line at the tightest pitch is an auto-grow
|
|
501
521
|
# floor, not a ceiling (Hancom grows the row past it); a merged row-span's
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
from __future__ import annotations
|
|
5
5
|
|
|
6
6
|
from io import BytesIO
|
|
7
|
+
import re
|
|
7
8
|
from typing import Mapping
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
import lxml.etree as etree
|
|
10
11
|
|
|
11
12
|
from .security import guard_xml_bytes, guard_xml_depth
|
|
12
13
|
|
|
@@ -25,19 +26,36 @@ _HWPML_2016_TO_2011: tuple[tuple[bytes, bytes], ...] = (
|
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
_XML_MARKUP = re.compile(
|
|
30
|
+
rb"<!--.*?-->|<!\[CDATA\[.*?\]\]>|<\?.*?\?>|<[^!?](?:[^>\"']|\"[^\"]*\"|'[^']*')*>",
|
|
31
|
+
re.S,
|
|
32
|
+
)
|
|
33
|
+
_XML_ATTRIBUTE = re.compile(rb"([\w:.-]+)(\s*=\s*)([\"'])(.*?)\3", re.S)
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
def normalize_hwpml_namespaces(data: bytes) -> bytes:
|
|
29
|
-
"""
|
|
37
|
+
"""Normalize namespace declarations, preserving ordinary URI-valued data.
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
won't appear.
|
|
39
|
+
In particular, hp:case/@hp:required-namespace is a feature-selection value,
|
|
40
|
+
not a namespace declaration. Rewriting it can select the wrong switch case.
|
|
41
|
+
Comments, CDATA, processing instructions and quoted non-xmlns values stay
|
|
42
|
+
byte-identical. The parser still uses the canonical 2011 namespace family.
|
|
36
43
|
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
mapping = dict(_HWPML_2016_TO_2011)
|
|
45
|
+
|
|
46
|
+
def attribute(match: re.Match[bytes]) -> bytes:
|
|
47
|
+
name, equals, quote, value = match.groups()
|
|
48
|
+
if name == b"xmlns" or name.startswith(b"xmlns:"):
|
|
49
|
+
value = mapping.get(value, value)
|
|
50
|
+
return name + equals + quote + value + quote
|
|
51
|
+
|
|
52
|
+
def markup(match: re.Match[bytes]) -> bytes:
|
|
53
|
+
tag = match.group()
|
|
54
|
+
if tag.startswith((b"<!", b"<?")):
|
|
55
|
+
return tag
|
|
56
|
+
return _XML_ATTRIBUTE.sub(attribute, tag)
|
|
57
|
+
|
|
58
|
+
return _XML_MARKUP.sub(markup, data)
|
|
41
59
|
|
|
42
60
|
|
|
43
61
|
def parse_xml(data: bytes) -> etree._Element:
|
|
@@ -1303,6 +1303,33 @@ class HwpxOxmlShape:
|
|
|
1303
1303
|
|
|
1304
1304
|
# --- size access -------------------------------------------------------
|
|
1305
1305
|
|
|
1306
|
+
def set_position(self, *, horizontal_offset: int, vertical_offset: int) -> None:
|
|
1307
|
+
"""Set an existing floating shape's offsets, in HWP units.
|
|
1308
|
+
|
|
1309
|
+
The existing reference frames, alignment, anchor, geometry and size
|
|
1310
|
+
are preserved. Inline shapes and missing positioning metadata refuse
|
|
1311
|
+
before mutation: changing their offsets would not move the drawing.
|
|
1312
|
+
"""
|
|
1313
|
+
from ..errors import HwpxValueError
|
|
1314
|
+
|
|
1315
|
+
for value in (horizontal_offset, vertical_offset):
|
|
1316
|
+
if isinstance(value, bool) or not isinstance(value, int) or not -(2**31) <= value < 2**31:
|
|
1317
|
+
raise HwpxValueError(
|
|
1318
|
+
"shape offsets must be signed 32-bit integer HWP units",
|
|
1319
|
+
code="shape-position-value",
|
|
1320
|
+
suggestion="Pass integer offsets in the shape's existing reference frame.",
|
|
1321
|
+
)
|
|
1322
|
+
position = self.element.find(f"{_HP}pos")
|
|
1323
|
+
if position is None or position.get("treatAsChar") not in {"0", "false", "False"}:
|
|
1324
|
+
raise HwpxValueError(
|
|
1325
|
+
"position editing requires an existing floating shape",
|
|
1326
|
+
code="shape-position-unsupported",
|
|
1327
|
+
suggestion="Inspect the anchor; inline placement is controlled by paragraph flow.",
|
|
1328
|
+
)
|
|
1329
|
+
position.set("horzOffset", str(horizontal_offset))
|
|
1330
|
+
position.set("vertOffset", str(vertical_offset))
|
|
1331
|
+
self.paragraph.section.mark_dirty()
|
|
1332
|
+
|
|
1306
1333
|
@property
|
|
1307
1334
|
def width(self) -> int:
|
|
1308
1335
|
sz = self.element.find(f"{_HP}sz")
|