python-hwpx 5.2.0__py3-none-any.whl → 5.3.0__py3-none-any.whl

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.
hwpx/_document/shapes.py CHANGED
@@ -282,3 +282,78 @@ def add_equation(
282
282
  raise RuntimeError(
283
283
  "created equation was not recognized by the standard section scan"
284
284
  )
285
+
286
+
287
+ def add_chart(
288
+ doc: "HwpxDocument",
289
+ chart_xml: bytes | str,
290
+ *,
291
+ paragraph: HwpxOxmlParagraph | None = None,
292
+ section: HwpxOxmlSection | None = None,
293
+ section_index: int | None = None,
294
+ size: tuple[int, int] | None = None,
295
+ treat_as_char: bool = False,
296
+ char_pr_id_ref: str | int | None = None,
297
+ ) -> HwpxOxmlInlineObject:
298
+ """Add a chartML part and its ``<hp:chart>`` anchor, then prove recognition.
299
+
300
+ The part is stored under ``Chart/chartN.xml`` and addressed directly by
301
+ the anchor's ``chartIDRef`` — real Hancom registers chart parts in no
302
+ manifest and draws the chart from the ECMA-376 chartML alone
303
+ (specs/055-chart-authoring/evidence/p0/chart-contract.md). The chartML is
304
+ validated to parse and to carry the ``c:chartSpace`` root before any part
305
+ is written; after insertion the anchor is re-read through the standard
306
+ section scan — creation fails loudly if it did not land (no
307
+ special-casing by design).
308
+ """
309
+ from lxml import etree as _etree # type: ignore[reportAttributeAccessIssue] # lxml has no complete bundled typing
310
+
311
+ from ..oxml.namespaces import HP
312
+
313
+ _CHART_SPACE = "{http://schemas.openxmlformats.org/drawingml/2006/chart}chartSpace"
314
+
315
+ data = chart_xml.encode("utf-8") if isinstance(chart_xml, str) else bytes(chart_xml)
316
+ if not data.strip():
317
+ raise ValueError("chart_xml must be non-empty chartML")
318
+ try:
319
+ root = _etree.fromstring(data)
320
+ except _etree.XMLSyntaxError as exc:
321
+ raise ValueError(f"chart_xml is not well-formed XML: {exc}") from exc
322
+ if root.tag != _CHART_SPACE:
323
+ raise ValueError(
324
+ "chart_xml root must be the ECMA-376 c:chartSpace element, "
325
+ f"got {root.tag!r}"
326
+ )
327
+
328
+ existing = {name for name in doc._package.part_names() if name.startswith("Chart/")}
329
+ n = 1
330
+ while f"Chart/chart{n}.xml" in existing:
331
+ n += 1
332
+ part_path = f"Chart/chart{n}.xml"
333
+
334
+ if paragraph is None:
335
+ paragraph = doc.add_paragraph(
336
+ "", section=section, section_index=section_index,
337
+ include_run=False,
338
+ )
339
+ doc._package.write(part_path, data)
340
+ inline_object = paragraph.add_chart(
341
+ part_path,
342
+ size=size,
343
+ treat_as_char=treat_as_char,
344
+ char_pr_id_ref=char_pr_id_ref,
345
+ )
346
+
347
+ created_id = inline_object.element.get("id", "")
348
+ for owning_section in doc.sections:
349
+ for candidate in owning_section.element.iter(f"{HP}chart"):
350
+ if candidate.get("id") != created_id:
351
+ continue
352
+ if candidate.get("chartIDRef") != part_path:
353
+ raise RuntimeError(
354
+ "created chart anchor does not reference its part"
355
+ )
356
+ return inline_object
357
+ raise RuntimeError(
358
+ "created chart was not recognized by the standard section scan"
359
+ )
hwpx/document.py CHANGED
@@ -1431,6 +1431,48 @@ class HwpxDocument:
1431
1431
  )
1432
1432
 
1433
1433
 
1434
+ def add_chart(
1435
+ self,
1436
+ chart_xml: bytes | str,
1437
+ *,
1438
+ paragraph: HwpxOxmlParagraph | None = None,
1439
+ section: HwpxOxmlSection | None = None,
1440
+ section_index: int | None = None,
1441
+ size: tuple[int, int] | None = None,
1442
+ treat_as_char: bool = False,
1443
+ char_pr_id_ref: str | int | None = None,
1444
+ ) -> HwpxOxmlInlineObject:
1445
+ """Insert a native chart from ECMA-376 chartML. **Experimental contract.**
1446
+
1447
+ Stores *chart_xml* as a ``Chart/chartN.xml`` package part and emits the
1448
+ real-Hancom ``<hp:chart>`` anchor referencing it via ``chartIDRef``
1449
+ (contract: ``specs/055-chart-authoring/evidence/p0/chart-contract.md``).
1450
+ Hancom draws the chart from the chartML alone — no OLE fallback or
1451
+ pre-rendered image is written. The chartML must parse and carry the
1452
+ ``c:chartSpace`` root (typed rejection otherwise), and the created
1453
+ anchor is re-read through the standard section scan — creation fails
1454
+ loudly if the standard consumer would not see it.
1455
+
1456
+ Args:
1457
+ chart_xml: ECMA-376 chartML document (``c:chartSpace``).
1458
+ paragraph: Target paragraph (e.g. inside a table cell). When
1459
+ omitted a new paragraph is appended to *section*.
1460
+ size: Optional ``(width, height)`` HWPUNIT pair for the anchor.
1461
+ treat_as_char: ``True`` places the chart inline in the text flow;
1462
+ default mirrors the render-verified gold float placement.
1463
+ """
1464
+
1465
+ return _shapes.add_chart(
1466
+ self,
1467
+ chart_xml,
1468
+ paragraph=paragraph,
1469
+ section=section,
1470
+ section_index=section_index,
1471
+ size=size,
1472
+ treat_as_char=treat_as_char,
1473
+ char_pr_id_ref=char_pr_id_ref,
1474
+ )
1475
+
1434
1476
  def add_equation(
1435
1477
  self,
1436
1478
  script: str,
hwpx/oxml/paragraph.py CHANGED
@@ -834,6 +834,78 @@ class HwpxOxmlParagraph:
834
834
  self.section.mark_dirty()
835
835
  return HwpxOxmlInlineObject(ctrl1, self)
836
836
 
837
+ def add_chart(
838
+ self,
839
+ chart_id_ref: str,
840
+ *,
841
+ size: tuple[int, int] | None = None,
842
+ treat_as_char: bool = False,
843
+ char_pr_id_ref: str | int | None = None,
844
+ run_attributes: dict[str, str] | None = None,
845
+ ) -> HwpxOxmlInlineObject:
846
+ """Insert a native ``<hp:chart>`` anchor referencing a chartML part.
847
+
848
+ Emits the real-Hancom chart anchor (contract reverse-engineered in
849
+ specs/055-chart-authoring/evidence/p0/chart-contract.md): the chart
850
+ content lives in an ECMA-376 chartML part addressed directly by
851
+ ``chartIDRef`` (real Hancom registers the part in no manifest), and
852
+ Hancom lays the chart out from the chartML alone — no OLE fallback or
853
+ pre-rendered image is required.
854
+
855
+ Args:
856
+ chart_id_ref: Package path of the chartML part
857
+ (e.g. ``Chart/chart1.xml``).
858
+ size: Optional ``(width, height)`` HWPUNIT pair for ``<hp:sz>``;
859
+ defaults to the gold document's 32250x18750.
860
+ treat_as_char: ``True`` places the chart inline in the text flow;
861
+ the default mirrors the gold float placement.
862
+ """
863
+ reference = chart_id_ref.strip()
864
+ if not reference:
865
+ raise ValueError("chart_id_ref must be a non-empty package path")
866
+ width, height = size if size is not None else (32250, 18750)
867
+ run = self._create_run_for_object(
868
+ run_attributes, char_pr_id_ref=char_pr_id_ref
869
+ )
870
+ chart = _append_child(run, f"{_HP}chart", {
871
+ "id": _object_id(),
872
+ "zOrder": "0",
873
+ "numberingType": "PICTURE",
874
+ "textWrap": "TOP_AND_BOTTOM" if treat_as_char else "SQUARE",
875
+ "textFlow": "BOTH_SIDES",
876
+ "lock": "0",
877
+ "dropcapstyle": "None",
878
+ "chartIDRef": reference,
879
+ })
880
+ _append_child(chart, f"{_HP}sz", {
881
+ "width": str(width),
882
+ "widthRelTo": "ABSOLUTE",
883
+ "height": str(height),
884
+ "heightRelTo": "ABSOLUTE",
885
+ "protect": "0",
886
+ })
887
+ _append_child(chart, f"{_HP}pos", {
888
+ "treatAsChar": "1" if treat_as_char else "0",
889
+ "affectLSpacing": "0",
890
+ "flowWithText": "1",
891
+ "allowOverlap": "0",
892
+ "holdAnchorAndSO": "0",
893
+ "vertRelTo": "PARA",
894
+ "horzRelTo": "PARA" if treat_as_char else "COLUMN",
895
+ "vertAlign": "TOP",
896
+ "horzAlign": "LEFT",
897
+ "vertOffset": "0",
898
+ "horzOffset": "0",
899
+ })
900
+ _append_child(chart, f"{_HP}outMargin", {
901
+ "left": "0",
902
+ "right": "0",
903
+ "top": "0",
904
+ "bottom": "0",
905
+ })
906
+ self.section.mark_dirty()
907
+ return HwpxOxmlInlineObject(chart, self)
908
+
837
909
  def add_equation(
838
910
  self,
839
911
  script: str,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-hwpx
3
- Version: 5.2.0
3
+ Version: 5.3.0
4
4
  Summary: 한글 없이 HWPX 문서를 열고, 편집하고, 생성하고, 검증하는 Python 문서 라이브러리
5
5
  Author: python-hwpx Maintainers
6
6
  License-Expression: Apache-2.0
@@ -1,6 +1,6 @@
1
1
  hwpx/__init__.py,sha256=WmlWHk6vfjr2HQa6Dzp6rmIXIUJXKWKDZJZ-LBpL3Nc,15936
2
2
  hwpx/body_patch.py,sha256=XfwrTMThS9vPLA62PDnEleCGuvYv61URxelbD_LHe8w,25730
3
- hwpx/document.py,sha256=3N90Ra8krX4Skj5WUTDl4ezHf1q0yaBQCKqficnR1p8,64169
3
+ hwpx/document.py,sha256=ly12zmjR96_4itGwJw9xljDI3sowmrSsZZfHHn4Znio,65995
4
4
  hwpx/errors.py,sha256=l3QK2Izwkpm25ar7QnNQvyg0yosoLfQt7kW5wn5MCG0,3033
5
5
  hwpx/experimental.py,sha256=BfD3fvXDgiEEm8G66TuUe_OPETitGwFU0VurUXUuV9Q,1693
6
6
  hwpx/mutation_report.py,sha256=6hurhDdgGiONeLIeWZJT8lwLtWJZ1VHp1l6G0zy0mQo,19409
@@ -16,7 +16,7 @@ hwpx/_document/layout.py,sha256=TicBhdxXW2lQkLyG5bg6PHW-OP-X4oWdP6XAgAXw8mg,2308
16
16
  hwpx/_document/media.py,sha256=kamsFJZ4K9aoKhjyztMHC64MVFR_ZQ50rDVgouZqxFA,11592
17
17
  hwpx/_document/memos.py,sha256=_tscEhNfPe9-iAkcEtJGjwHTN42_lx4stFoPrgJxk3o,7481
18
18
  hwpx/_document/persistence.py,sha256=NgWeFmm6jWV11194kdx32lwRQ6dyqiQYnc1ZBzJcIbE,16003
19
- hwpx/_document/shapes.py,sha256=XoCp41A6AmemOv9tGSMXRyu7sE7SMVBSzXSZfkelKWA,8825
19
+ hwpx/_document/shapes.py,sha256=NC3eaA4H2cXDdTi3REMaSZifXqwpChAhS4LPGCSDUc0,11702
20
20
  hwpx/_document/tracked.py,sha256=1sQ9Q3-_wOAH2SQPbJQAeCG9E9HjV3y1gCORzrPUEPo,5010
21
21
  hwpx/data/Skeleton.hwpx,sha256=yR-3epkzo-VkWiV3ji0rmemBN-2ZVypdHdKe8MD9TJg,7490
22
22
  hwpx/equation/__init__.py,sha256=0wks0_pS_tBokCVMeWUJGE6tbW35Ptc0I5Z_36FY-Jo,1328
@@ -56,7 +56,7 @@ hwpx/oxml/memo.py,sha256=QlmuELEzywnmywgjm94S3vzJB8wPpoFOmI8KVuzoNPw,9484
56
56
  hwpx/oxml/namespaces.py,sha256=c7JfdOdJbzrhyHvjbxoeeeloRE3xPuoB7v3YI8SmKDk,6524
57
57
  hwpx/oxml/numbering.py,sha256=9a0ARGW1DkKsTF7mEEXM4FA5loQmzIBuqE4APA8UFIQ,669
58
58
  hwpx/oxml/objects.py,sha256=8ErDXWvkjLm3FTsx0Z_IovUXbiv2uc-j0B5Y2-OtihA,20864
59
- hwpx/oxml/paragraph.py,sha256=U-bGkgMs3wiEBE1TXW6jSJj0SYwzN9xtWeOyOhLLJt0,44238
59
+ hwpx/oxml/paragraph.py,sha256=p9M22WLPt3_mjmQZGrpSS9agRDKAXwSvnKJGnCUgc48,47075
60
60
  hwpx/oxml/parser.py,sha256=pIfyNdW3WdFkCcE8JFY7hn1fobRbMngzfhVZICvWeVs,2937
61
61
  hwpx/oxml/run.py,sha256=F939J1W6zQjr-JQ1Z8-nKh2s7NQ_0Okov4QJAEyw_5M,15503
62
62
  hwpx/oxml/schema.py,sha256=ElR3_IIhhPPZEqJtNKMNCHA-VFVLdhL454W_4spuvlc,1284
@@ -104,10 +104,10 @@ hwpx/tools/toc_fidelity.py,sha256=rvoKH8QJ65WNVrxS0d-i2AtODvBy-4O8NRudfQ244v4,19
104
104
  hwpx/tools/validator.py,sha256=U856izL9NcJZOiKDYoCpwOSaFNQ2Un8Jn4pzL20A96Q,7100
105
105
  hwpx/tools/_schemas/header.xsd,sha256=mJXuFMuHGT1JnFFaluUpYUglwjMCNlfbFCRVM26eHXE,664
106
106
  hwpx/tools/_schemas/section.xsd,sha256=MgvavVHG05RDfUnVPxVU10H4FQOja5ON04_m9Uk_m7E,522
107
- python_hwpx-5.2.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
108
- python_hwpx-5.2.0.dist-info/licenses/NOTICE,sha256=auRgKYGdrOgWrj4kZfAuXQnqgDnx5myI9TI17IztIzQ,3466
109
- python_hwpx-5.2.0.dist-info/METADATA,sha256=dTDpJNzdORyUZnVj8fANpARJ74j3QOekvJ9Mu_O5mYc,13162
110
- python_hwpx-5.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
111
- python_hwpx-5.2.0.dist-info/entry_points.txt,sha256=JUKRxbly9UaeHV7YzOea23y8IiqSTcrhUlooP3fS_Zc,405
112
- python_hwpx-5.2.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
113
- python_hwpx-5.2.0.dist-info/RECORD,,
107
+ python_hwpx-5.3.0.dist-info/licenses/LICENSE,sha256=_ubz4wv-BkkT3l3gu-QuH7JGeVjuRYGZoZK95eNsCHU,9688
108
+ python_hwpx-5.3.0.dist-info/licenses/NOTICE,sha256=auRgKYGdrOgWrj4kZfAuXQnqgDnx5myI9TI17IztIzQ,3466
109
+ python_hwpx-5.3.0.dist-info/METADATA,sha256=ve8a6QxqNSMJ6H-PbMfQOS6OpFREBe8Xfe1jEWER33U,13162
110
+ python_hwpx-5.3.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
111
+ python_hwpx-5.3.0.dist-info/entry_points.txt,sha256=JUKRxbly9UaeHV7YzOea23y8IiqSTcrhUlooP3fS_Zc,405
112
+ python_hwpx-5.3.0.dist-info/top_level.txt,sha256=R1iToqDh80Nf2oQhRjTN0rbN2X6kyDUizIocZjkhuxc,5
113
+ python_hwpx-5.3.0.dist-info/RECORD,,