kaxe 1.4.4.dev4__tar.gz → 1.4.4.dev5__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.
Files changed (131) hide show
  1. {kaxe-1.4.4.dev4/src/kaxe.egg-info → kaxe-1.4.4.dev5}/PKG-INFO +2 -1
  2. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/pyproject.toml +2 -2
  3. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/svg_pdf.py +250 -40
  4. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5/src/kaxe.egg-info}/PKG-INFO +2 -1
  5. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe.egg-info/requires.txt +1 -0
  6. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/LICENSE +0 -0
  7. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/MANIFEST.in +0 -0
  8. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/README.md +0 -0
  9. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/setup.cfg +0 -0
  10. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/__init__.py +0 -0
  11. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/chart/__init__.py +0 -0
  12. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/chart/bar.py +0 -0
  13. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/chart/box.py +0 -0
  14. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/chart/pie.py +0 -0
  15. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/chart/qqplot.py +0 -0
  16. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/__init__.py +0 -0
  17. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/axis.py +0 -0
  18. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/color.py +0 -0
  19. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/backend.py +0 -0
  20. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/camera.py +0 -0
  21. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/helper.py +0 -0
  22. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/hud.py +0 -0
  23. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/objects/__init__.py +0 -0
  24. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/objects/color.py +0 -0
  25. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/objects/line.py +0 -0
  26. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/objects/point.py +0 -0
  27. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/objects/pointer.py +0 -0
  28. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/objects/triangle.py +0 -0
  29. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/openglrender.py +0 -0
  30. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/d3/translator.py +0 -0
  31. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/draw.py +0 -0
  32. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/fileloader.py +0 -0
  33. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/helper.py +0 -0
  34. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/legend.py +0 -0
  35. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/line.py +0 -0
  36. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/marker.py +0 -0
  37. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/profiler.py +0 -0
  38. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/round.py +0 -0
  39. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/shapes.py +0 -0
  40. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/styles.py +0 -0
  41. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/svg.py +0 -0
  42. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/symbol.py +0 -0
  43. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/text.py +0 -0
  44. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/core/window.py +0 -0
  45. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/data/__init__.py +0 -0
  46. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/data/excel.py +0 -0
  47. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/__init__.py +0 -0
  48. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/_lazy.py +0 -0
  49. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/__init__.py +0 -0
  50. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/arrow.py +0 -0
  51. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/bubble.py +0 -0
  52. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/contour.py +0 -0
  53. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/equation.py +0 -0
  54. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/fill.py +0 -0
  55. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/function.py +0 -0
  56. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/map.py +0 -0
  57. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/parameter.py +0 -0
  58. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/pillar.py +0 -0
  59. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d2/point.py +0 -0
  60. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d3/__init__.py +0 -0
  61. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d3/base.py +0 -0
  62. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d3/function.py +0 -0
  63. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d3/mesh.py +0 -0
  64. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d3/point.py +0 -0
  65. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/d3/potato.py +0 -0
  66. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/function.py +0 -0
  67. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/legend.py +0 -0
  68. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/mapdata.py +0 -0
  69. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/point.py +0 -0
  70. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/objects/text.py +0 -0
  71. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/__init__.py +0 -0
  72. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/_lazy.py +0 -0
  73. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/box.py +0 -0
  74. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/constants.py +0 -0
  75. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/d3/__init__.py +0 -0
  76. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/d3/axes.py +0 -0
  77. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/d3/geometry.py +0 -0
  78. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/d3/plot3d.py +0 -0
  79. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/d3/variants.py +0 -0
  80. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/double.py +0 -0
  81. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/empty.py +0 -0
  82. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/grid.py +0 -0
  83. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/log.py +0 -0
  84. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/polar.py +0 -0
  85. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/standard.py +0 -0
  86. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/themes.py +0 -0
  87. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/zoom.py +0 -0
  88. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/plot/zoom_connector.py +0 -0
  89. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/__init__.py +0 -0
  90. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/__init__.py +0 -0
  91. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.bright-oblique.ttf +0 -0
  92. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.bright-roman.ttf +0 -0
  93. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.bright-semibold.ttf +0 -0
  94. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.bright-semiboldoblique.ttf +0 -0
  95. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.classical-serif-italic.ttf +0 -0
  96. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.concrete-bold.ttf +0 -0
  97. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.concrete-bolditalic.ttf +0 -0
  98. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.concrete-italic.ttf +0 -0
  99. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.concrete-roman.ttf +0 -0
  100. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-bold.ttf +0 -0
  101. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-boldoblique.ttf +0 -0
  102. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-demi-condensed-demicondensed.ttf +0 -0
  103. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-medium.ttf +0 -0
  104. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.sans-serif-oblique.ttf +0 -0
  105. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-bold.ttf +0 -0
  106. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-bolditalic.ttf +0 -0
  107. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-boldslanted.ttf +0 -0
  108. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-extra-romanslanted.ttf +0 -0
  109. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-italic.ttf +0 -0
  110. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-roman.ttf +0 -0
  111. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.serif-upright-italic-uprightitalic.ttf +0 -0
  112. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bold.ttf +0 -0
  113. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-bolditalic.ttf +0 -0
  114. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-italic.ttf +0 -0
  115. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-light.ttf +0 -0
  116. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-lightoblique.ttf +0 -0
  117. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-regular.ttf +0 -0
  118. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-italic.ttf +0 -0
  119. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/cmu.typewriter-text-variable-width-medium.ttf +0 -0
  120. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/computer-modern-family/readme.txt +0 -0
  121. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/logo-small.png +0 -0
  122. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/symbolcross.png +0 -0
  123. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/symboldonut.png +0 -0
  124. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/symbollollipop.png +0 -0
  125. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe/resources/symboltriangle.png +0 -0
  126. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe.egg-info/SOURCES.txt +0 -0
  127. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe.egg-info/dependency_links.txt +0 -0
  128. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/src/kaxe.egg-info/top_level.txt +0 -0
  129. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/tests/test.py +0 -0
  130. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/tests/test2.py +0 -0
  131. {kaxe-1.4.4.dev4 → kaxe-1.4.4.dev5}/tests/test3.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaxe
3
- Version: 1.4.4.dev4
3
+ Version: 1.4.4.dev5
4
4
  Summary: A small graphing tool for functions, points, equations and more
5
5
  Author-email: Valter Yde Daugberg <valteryde@hotmail.com>
6
6
  Project-URL: Homepage, https://github.com/valteryde/kaxe
@@ -27,6 +27,7 @@ Requires-Dist: psutil
27
27
  Requires-Dist: opencv-python-headless
28
28
  Provides-Extra: pdf
29
29
  Requires-Dist: reportlab>=4.0; extra == "pdf"
30
+ Requires-Dist: fonttools>=4.0; extra == "pdf"
30
31
  Dynamic: license-file
31
32
 
32
33
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kaxe"
3
- version = "1.4.4.dev4"
3
+ version = "1.4.4.dev5"
4
4
  authors = [
5
5
  { name="Valter Yde Daugberg", email="valteryde@hotmail.com" },
6
6
  ]
@@ -30,7 +30,7 @@ dependencies = [
30
30
  ]
31
31
 
32
32
  [project.optional-dependencies]
33
- pdf = ["reportlab>=4.0"]
33
+ pdf = ["reportlab>=4.0", "fonttools>=4.0"]
34
34
 
35
35
  [project.urls]
36
36
  Homepage = "https://github.com/valteryde/kaxe"
@@ -21,6 +21,7 @@ if TYPE_CHECKING:
21
21
 
22
22
  _PDF_INSTALL_HINT = "PDF export requires reportlab. Install with: pip install kaxe[pdf]"
23
23
  _FONDI_FONTS_REGISTERED = False
24
+ _OTF_TTF_CACHE: dict[str, bytes] = {}
24
25
 
25
26
  _TRANSFORM_RE = re.compile(
26
27
  r"(translate|rotate|matrix)\s*\(([^)]*)\)"
@@ -71,6 +72,90 @@ def _rl_color(value: Optional[str], opacity: Optional[str] = None):
71
72
  return Color(r / 255.0, g / 255.0, b / 255.0, alpha=alpha)
72
73
 
73
74
 
75
+ def _require_fonttools():
76
+ try:
77
+ import fontTools # noqa: F401
78
+ except ImportError as exc:
79
+ raise ImportError(
80
+ "PDF font embedding requires fonttools. Install with: pip install kaxe[pdf]"
81
+ ) from exc
82
+
83
+
84
+ def _convert_otf_to_ttf_bytes(otf_path: os.PathLike[str]) -> bytes:
85
+ path = os.fspath(otf_path)
86
+ cached = _OTF_TTF_CACHE.get(path)
87
+ if cached is not None:
88
+ return cached
89
+
90
+ _require_fonttools()
91
+ from fontTools.pens.cu2quPen import Cu2QuPen
92
+ from fontTools.pens.ttGlyphPen import TTGlyphPen
93
+ from fontTools.ttLib import TTFont, newTable
94
+
95
+ def _glyphs_to_quadratic(glyphs):
96
+ quad: dict = {}
97
+ for gname in glyphs.keys():
98
+ tt_pen = TTGlyphPen(glyphs)
99
+ cu2qu_pen = Cu2QuPen(tt_pen, 1.0, reverse_direction=True)
100
+ glyphs[gname].draw(cu2qu_pen)
101
+ quad[gname] = tt_pen.glyph()
102
+ return quad
103
+
104
+ font = TTFont(path)
105
+ if font.sfntVersion != "OTTO" or "CFF " not in font:
106
+ buf = io.BytesIO()
107
+ font.save(buf)
108
+ data = buf.getvalue()
109
+ _OTF_TTF_CACHE[path] = data
110
+ return data
111
+
112
+ glyph_order = font.getGlyphOrder()
113
+ font["loca"] = newTable("loca")
114
+ glyf = newTable("glyf")
115
+ glyf.glyphOrder = glyph_order
116
+ glyf.glyphs = _glyphs_to_quadratic(font.getGlyphSet())
117
+ font["glyf"] = glyf
118
+ del font["CFF "]
119
+ glyf.compile(font)
120
+
121
+ hmtx = font["hmtx"]
122
+ for glyph_name, glyph in glyf.glyphs.items():
123
+ if hasattr(glyph, "xMin"):
124
+ hmtx[glyph_name] = (hmtx[glyph_name][0], glyph.xMin)
125
+
126
+ maxp = newTable("maxp")
127
+ maxp.tableVersion = 0x00010000
128
+ maxp.maxZones = 1
129
+ maxp.maxTwilightPoints = 0
130
+ maxp.maxStorage = 0
131
+ maxp.maxFunctionDefs = 0
132
+ maxp.maxInstructionDefs = 0
133
+ maxp.maxStackElements = 0
134
+ maxp.maxSizeOfInstructions = 0
135
+ maxp.maxComponentElements = max(
136
+ len(g.components if hasattr(g, "components") else [])
137
+ for g in glyf.glyphs.values()
138
+ )
139
+ maxp.compile(font)
140
+
141
+ post = font["post"]
142
+ post.formatType = 2.0
143
+ post.extraNames = []
144
+ post.mapping = {}
145
+ post.glyphOrder = glyph_order
146
+ try:
147
+ post.compile(font)
148
+ except OverflowError:
149
+ post.formatType = 3
150
+
151
+ font.sfntVersion = "\000\001\000\000"
152
+ buf = io.BytesIO()
153
+ font.save(buf)
154
+ data = buf.getvalue()
155
+ _OTF_TTF_CACHE[path] = data
156
+ return data
157
+
158
+
74
159
  def _register_fondi_fonts() -> None:
75
160
  global _FONDI_FONTS_REGISTERED
76
161
  if _FONDI_FONTS_REGISTERED:
@@ -83,22 +168,49 @@ def _register_fondi_fonts() -> None:
83
168
  from reportlab.pdfbase.ttfonts import TTFont
84
169
 
85
170
  resources = Path(fondi.__file__).parent / "resources"
86
- regular_ttf = resources / "cmu.serif-roman.ttf"
87
- if not regular_ttf.is_file():
88
- logging.warning("Fondi regular TTF not found; PDF math labels will use Helvetica")
171
+ regular_otf = resources / "NewCM10-Regular.otf"
172
+ italic_otf = resources / "NewCM10-Italic.otf"
173
+ fallback_ttf = resources / "cmu.serif-roman.ttf"
174
+
175
+ def _register(name: str, otf: Path, ttf_fallback: Optional[Path] = None) -> bool:
176
+ for candidate in (otf, ttf_fallback):
177
+ if not candidate or not candidate.is_file():
178
+ continue
179
+ try:
180
+ if candidate.suffix.lower() == ".otf":
181
+ data = _convert_otf_to_ttf_bytes(candidate)
182
+ else:
183
+ data = candidate.read_bytes()
184
+ pdfmetrics.registerFont(TTFont(name, io.BytesIO(data)))
185
+ return True
186
+ except Exception as exc:
187
+ logging.warning("Could not register PDF font %s from %s: %s", name, candidate, exc)
188
+ return False
189
+
190
+ if not _register("FondiNewCM", regular_otf, fallback_ttf):
191
+ logging.warning("Fondi regular font unavailable; PDF math labels will use Helvetica")
89
192
  _FONDI_FONTS_REGISTERED = True
90
193
  return
91
194
 
92
- pdfmetrics.registerFont(TTFont("FondiNewCM", str(regular_ttf)))
93
- pdfmetrics.registerFontFamily(
94
- "FondiNewCM",
95
- normal="FondiNewCM",
96
- italic="FondiNewCM",
97
- )
195
+ if _register("FondiNewCM-Italic", italic_otf):
196
+ pdfmetrics.registerFontFamily(
197
+ "FondiNewCM",
198
+ normal="FondiNewCM",
199
+ italic="FondiNewCM-Italic",
200
+ )
201
+ else:
202
+ pdfmetrics.registerFontFamily(
203
+ "FondiNewCM",
204
+ normal="FondiNewCM",
205
+ italic="FondiNewCM",
206
+ )
207
+
98
208
  _FONDI_FONTS_REGISTERED = True
99
209
 
100
210
 
101
211
  def _resolve_font_name(family: Optional[str], style: Optional[str]) -> str:
212
+ if family == "FondiNewCM" and style == "italic":
213
+ return "FondiNewCM-Italic"
102
214
  if family == "FondiNewCM":
103
215
  return "FondiNewCM"
104
216
  if family:
@@ -132,6 +244,31 @@ class _Matrix:
132
244
  )
133
245
 
134
246
 
247
+ def _is_identity(matrix: _Matrix, tol: float = 1e-9) -> bool:
248
+ return (
249
+ abs(matrix.a - 1.0) < tol
250
+ and abs(matrix.d - 1.0) < tol
251
+ and abs(matrix.b) < tol
252
+ and abs(matrix.c) < tol
253
+ and abs(matrix.e) < tol
254
+ and abs(matrix.f) < tol
255
+ )
256
+
257
+
258
+ def _matrix_to_reportlab_transform(matrix: _Matrix, height: int, *, nested: bool) -> list[float]:
259
+ """Map SVG-local y-down coords to ReportLab parent coords via Group.transform."""
260
+ # Conjugate y-down SVG affine with flip-to-y-up: T(height) * M * F, F:(x,y)->(x,-y)
261
+ f = height if not nested else 0.0
262
+ return [
263
+ matrix.a,
264
+ -matrix.b,
265
+ -matrix.c,
266
+ matrix.d,
267
+ matrix.e,
268
+ f + matrix.f,
269
+ ]
270
+
271
+
135
272
  def _parse_transform(transform: Optional[str]) -> _Matrix:
136
273
  matrix = _Matrix()
137
274
  if not transform:
@@ -178,14 +315,29 @@ class _PdfBuilder:
178
315
  self.drawing = Drawing(width, height)
179
316
  self._shapes: list[Any] = []
180
317
 
181
- def _to_rl(self, matrix: _Matrix, x: float, y: float) -> tuple[float, float]:
318
+ def _to_rl(
319
+ self,
320
+ matrix: _Matrix,
321
+ x: float,
322
+ y: float,
323
+ *,
324
+ local: bool = False,
325
+ ) -> tuple[float, float]:
326
+ if local:
327
+ return x, -y
182
328
  sx, sy = matrix.map_point(x, y)
183
329
  return sx, self.height - sy
184
330
 
185
331
  def _add(self, shape) -> None:
186
332
  self._shapes.append(shape)
187
333
 
188
- def _render_element(self, el: ET.Element, matrix: _Matrix) -> None:
334
+ def _render_element(
335
+ self,
336
+ el: ET.Element,
337
+ matrix: _Matrix,
338
+ *,
339
+ local: bool = False,
340
+ ) -> None:
189
341
  from reportlab.graphics.shapes import (
190
342
  Circle,
191
343
  Group,
@@ -203,13 +355,28 @@ class _PdfBuilder:
203
355
  opacity = _attr(el, "opacity")
204
356
 
205
357
  if tag == "g":
206
- child_matrix = matrix.multiply(_parse_transform(_attr(el, "transform")))
358
+ local_transform = _parse_transform(_attr(el, "transform"))
359
+ if _is_identity(local_transform):
360
+ for child in el:
361
+ self._render_element(
362
+ child,
363
+ matrix.multiply(local_transform),
364
+ local=local,
365
+ )
366
+ return
367
+
368
+ combined = matrix.multiply(local_transform)
207
369
  group = Group()
370
+ group.transform = _matrix_to_reportlab_transform(
371
+ combined if not local else local_transform,
372
+ self.height,
373
+ nested=local,
374
+ )
208
375
  child_shapes: list[Any] = []
209
376
  saved = self._shapes
210
377
  self._shapes = child_shapes
211
378
  for child in el:
212
- self._render_element(child, child_matrix)
379
+ self._render_element(child, _Matrix(), local=True)
213
380
  self._shapes = saved
214
381
  for shape in child_shapes:
215
382
  group.add(shape)
@@ -222,8 +389,8 @@ class _PdfBuilder:
222
389
  y = _parse_float(_attr(el, "y"))
223
390
  w = _parse_float(_attr(el, "width"))
224
391
  h = _parse_float(_attr(el, "height"))
225
- x0, y0 = self._to_rl(matrix, x, y)
226
- x1, y1 = self._to_rl(matrix, x + w, y + h)
392
+ x0, y0 = self._to_rl(matrix, x, y, local=local)
393
+ x1, y1 = self._to_rl(matrix, x + w, y + h, local=local)
227
394
  rect = Rect(
228
395
  min(x0, x1),
229
396
  min(y0, y1),
@@ -236,15 +403,15 @@ class _PdfBuilder:
236
403
  return
237
404
 
238
405
  if tag == "line":
239
- x1, y1 = self._to_rl(matrix, _parse_float(_attr(el, "x1")), _parse_float(_attr(el, "y1")))
240
- x2, y2 = self._to_rl(matrix, _parse_float(_attr(el, "x2")), _parse_float(_attr(el, "y2")))
406
+ x1, y1 = self._to_rl(matrix, _parse_float(_attr(el, "x1")), _parse_float(_attr(el, "y1")), local=local)
407
+ x2, y2 = self._to_rl(matrix, _parse_float(_attr(el, "x2")), _parse_float(_attr(el, "y2")), local=local)
241
408
  stroke = _rl_color(_attr(el, "stroke"), opacity)
242
409
  width = _parse_float(_attr(el, "stroke-width"), 1.0)
243
410
  self._add(Line(x1, y1, x2, y2, strokeColor=stroke, strokeWidth=width))
244
411
  return
245
412
 
246
413
  if tag == "circle":
247
- cx, cy = self._to_rl(matrix, _parse_float(_attr(el, "cx")), _parse_float(_attr(el, "cy")))
414
+ cx, cy = self._to_rl(matrix, _parse_float(_attr(el, "cx")), _parse_float(_attr(el, "cy")), local=local)
248
415
  radius = _parse_float(_attr(el, "r"))
249
416
  fill = _attr(el, "fill")
250
417
  if fill and fill != "none":
@@ -271,7 +438,7 @@ class _PdfBuilder:
271
438
  return
272
439
 
273
440
  if tag == "polygon":
274
- points = self._parse_points(_attr(el, "points"), matrix)
441
+ points = self._parse_points(_attr(el, "points"), matrix, local=local)
275
442
  self._add(
276
443
  Polygon(
277
444
  points,
@@ -282,7 +449,7 @@ class _PdfBuilder:
282
449
  return
283
450
 
284
451
  if tag == "polyline":
285
- points = self._parse_points(_attr(el, "points"), matrix)
452
+ points = self._parse_points(_attr(el, "points"), matrix, local=local)
286
453
  self._add(
287
454
  PolyLine(
288
455
  points,
@@ -294,7 +461,7 @@ class _PdfBuilder:
294
461
  return
295
462
 
296
463
  if tag == "path":
297
- path = self._parse_path(_attr(el, "d"), matrix)
464
+ path = self._parse_path(_attr(el, "d"), matrix, local=local)
298
465
  if path is not None:
299
466
  path.fillColor = _rl_color(_attr(el, "fill"), opacity)
300
467
  path.strokeColor = None
@@ -312,24 +479,58 @@ class _PdfBuilder:
312
479
  y = _parse_float(_attr(el, "y"))
313
480
  w = _parse_float(_attr(el, "width"), img.width)
314
481
  h = _parse_float(_attr(el, "height"), img.height)
482
+ image_transform = _parse_transform(_attr(el, "transform"))
483
+
484
+ def _add_image_shape() -> None:
485
+ x0, y0 = self._to_rl(_Matrix(), x, y, local=True)
486
+ x1, y1 = self._to_rl(_Matrix(), x + w, y + h, local=True)
487
+ rl_img = RlImage(
488
+ min(x0, x1),
489
+ min(y0, y1),
490
+ abs(x1 - x0),
491
+ abs(y1 - y0),
492
+ path=ImageReader(io.BytesIO(raw)),
493
+ )
494
+ self._add(rl_img)
495
+
496
+ if _is_identity(image_transform):
497
+ if local:
498
+ _add_image_shape()
499
+ else:
500
+ x0, y0 = self._to_rl(matrix, x, y, local=False)
501
+ x1, y1 = self._to_rl(matrix, x + w, y + h, local=False)
502
+ self._add(
503
+ RlImage(
504
+ min(x0, x1),
505
+ min(y0, y1),
506
+ abs(x1 - x0),
507
+ abs(y1 - y0),
508
+ path=ImageReader(io.BytesIO(raw)),
509
+ )
510
+ )
511
+ return
315
512
 
316
- image_matrix = matrix.multiply(_parse_transform(_attr(el, "transform")))
317
- x0, y0 = self._to_rl(image_matrix, x, y)
318
- x1, y1 = self._to_rl(image_matrix, x + w, y + h)
319
- rl_img = RlImage(
320
- min(x0, x1),
321
- min(y0, y1),
322
- abs(x1 - x0),
323
- abs(y1 - y0),
324
- path=ImageReader(io.BytesIO(raw)),
513
+ combined = matrix.multiply(image_transform)
514
+ group = Group()
515
+ group.transform = _matrix_to_reportlab_transform(
516
+ combined if not local else image_transform,
517
+ self.height,
518
+ nested=local,
325
519
  )
326
- self._add(rl_img)
520
+ child_shapes: list[Any] = []
521
+ saved = self._shapes
522
+ self._shapes = child_shapes
523
+ _add_image_shape()
524
+ self._shapes = saved
525
+ for shape in child_shapes:
526
+ group.add(shape)
527
+ self._add(group)
327
528
  return
328
529
 
329
530
  if tag == "text":
330
531
  x = _parse_float(_attr(el, "x"))
331
532
  y = _parse_float(_attr(el, "y"))
332
- rl_x, rl_y = self._to_rl(matrix, x, y)
533
+ rl_x, rl_y = self._to_rl(matrix, x, y, local=local)
333
534
  text = (el.text or "").strip()
334
535
  if not text:
335
536
  text = "".join(el.itertext())
@@ -350,17 +551,23 @@ class _PdfBuilder:
350
551
  )
351
552
  return
352
553
 
353
- def _parse_points(self, value: Optional[str], matrix: _Matrix) -> list[float]:
554
+ def _parse_points(
555
+ self,
556
+ value: Optional[str],
557
+ matrix: _Matrix,
558
+ *,
559
+ local: bool = False,
560
+ ) -> list[float]:
354
561
  if not value:
355
562
  return []
356
563
  nums = [float(n) for n in re.split(r"[\s,]+", value.strip()) if n]
357
564
  points: list[float] = []
358
565
  for i in range(0, len(nums) - 1, 2):
359
- x, y = self._to_rl(matrix, nums[i], nums[i + 1])
566
+ x, y = self._to_rl(matrix, nums[i], nums[i + 1], local=local)
360
567
  points.extend([x, y])
361
568
  return points
362
569
 
363
- def _parse_path(self, d: Optional[str], matrix: _Matrix):
570
+ def _parse_path(self, d: Optional[str], matrix: _Matrix, *, local: bool = False):
364
571
  from reportlab.graphics.shapes import Path
365
572
 
366
573
  if not d:
@@ -382,11 +589,11 @@ class _PdfBuilder:
382
589
  current_x, current_y = nums[0], nums[1]
383
590
  start_x, start_y = current_x, current_y
384
591
  subpath_start_x, subpath_start_y = current_x, current_y
385
- x, y = self._to_rl(matrix, current_x, current_y)
592
+ x, y = self._to_rl(matrix, current_x, current_y, local=local)
386
593
  path.moveTo(x, y)
387
594
  elif cmd == "L" and len(nums) >= 2:
388
595
  current_x, current_y = nums[0], nums[1]
389
- x, y = self._to_rl(matrix, current_x, current_y)
596
+ x, y = self._to_rl(matrix, current_x, current_y, local=local)
390
597
  path.lineTo(x, y)
391
598
  elif cmd == "A" and len(nums) >= 7:
392
599
  rx, ry, _, large_arc, sweep, end_x, end_y = nums[:7]
@@ -401,11 +608,12 @@ class _PdfBuilder:
401
608
  rx,
402
609
  bool(int(large_arc)),
403
610
  bool(int(sweep)),
611
+ local=local,
404
612
  )
405
613
  current_x, current_y = end_x, end_y
406
614
  elif cmd == "Z":
407
615
  current_x, current_y = subpath_start_x, subpath_start_y
408
- x, y = self._to_rl(matrix, current_x, current_y)
616
+ x, y = self._to_rl(matrix, current_x, current_y, local=local)
409
617
  path.lineTo(x, y)
410
618
  path.closePath()
411
619
 
@@ -422,12 +630,14 @@ class _PdfBuilder:
422
630
  radius: float,
423
631
  large_arc: bool,
424
632
  sweep: bool,
633
+ *,
634
+ local: bool = False,
425
635
  ) -> None:
426
636
  dx = x2 - x1
427
637
  dy = y2 - y1
428
638
  dist = math.hypot(dx, dy)
429
639
  if dist == 0 or radius == 0:
430
- x, y = self._to_rl(matrix, x2, y2)
640
+ x, y = self._to_rl(matrix, x2, y2, local=local)
431
641
  path.lineTo(x, y)
432
642
  return
433
643
 
@@ -479,7 +689,7 @@ class _PdfBuilder:
479
689
  t = start + (end - start) * (i / steps)
480
690
  px = cx + radius * math.cos(t)
481
691
  py = cy + radius * math.sin(t)
482
- x, y = self._to_rl(matrix, px, py)
692
+ x, y = self._to_rl(matrix, px, py, local=local)
483
693
  path.lineTo(x, y)
484
694
 
485
695
  def build(self, elements: list[ET.Element]):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kaxe
3
- Version: 1.4.4.dev4
3
+ Version: 1.4.4.dev5
4
4
  Summary: A small graphing tool for functions, points, equations and more
5
5
  Author-email: Valter Yde Daugberg <valteryde@hotmail.com>
6
6
  Project-URL: Homepage, https://github.com/valteryde/kaxe
@@ -27,6 +27,7 @@ Requires-Dist: psutil
27
27
  Requires-Dist: opencv-python-headless
28
28
  Provides-Extra: pdf
29
29
  Requires-Dist: reportlab>=4.0; extra == "pdf"
30
+ Requires-Dist: fonttools>=4.0; extra == "pdf"
30
31
  Dynamic: license-file
31
32
 
32
33
 
@@ -15,3 +15,4 @@ opencv-python-headless
15
15
 
16
16
  [pdf]
17
17
  reportlab>=4.0
18
+ fonttools>=4.0
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes