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