""" Convert text to HPGL using a font file (.otf, .ttf). Uses matplotlib TextPath; flattens curves to line segments. """ from __future__ import division import os # Path codes from matplotlib.path.Path MOVETO = 1 LINETO = 2 CURVE3 = 3 CURVE4 = 4 CLOSEPOLY = 79 def _flatten_quadratic(p0, p1, p2, n=10): """Quadratic Bezier: B(t) = (1-t)^2 P0 + 2(1-t)t P1 + t^2 P2.""" for i in range(1, n + 1): t = i / n u = 1 - t x = u * u * p0[0] + 2 * u * t * p1[0] + t * t * p2[0] y = u * u * p0[1] + 2 * u * t * p1[1] + t * t * p2[1] yield (x, y) def _flatten_cubic(p0, p1, p2, p3, n=10): """Cubic Bezier: B(t) = (1-t)^3 P0 + 3(1-t)^2 t P1 + 3(1-t)t^2 P2 + t^3 P3.""" for i in range(1, n + 1): t = i / n u = 1 - t x = u * u * u * p0[0] + 3 * u * u * t * p1[0] + 3 * u * t * t * p2[0] + t * t * t * p3[0] y = u * u * u * p0[1] + 3 * u * u * t * p1[1] + 3 * u * t * t * p2[1] + t * t * t * p3[1] yield (x, y) def text_to_hpgl(text, font_path, size_pt=72, units_per_pt=14): """ Convert text to HPGL string using the given font file. size_pt: font size in points. units_per_pt: HPGL units per point (default ~0.35 mm per pt). Returns HPGL string (IN;SP1;PU x,y;PD;PA x,y;...). """ try: from matplotlib.textpath import TextPath from matplotlib.font_manager import FontProperties except ImportError: raise RuntimeError('matplotlib is required for text-to-HPGL. Install with: pip install matplotlib') if not text or not font_path or not os.path.isfile(font_path): raise ValueError('Need non-empty text and a valid font file path.') prop = FontProperties(fname=font_path) path = TextPath((0, 0), text, size=size_pt, prop=prop) vertices = path.vertices codes = path.codes if codes is None: codes = [MOVETO] + [LINETO] * (len(vertices) - 1) # Scale to HPGL units (y flip: matplotlib y up, HPGL typically y up too; we keep same) scale = units_per_pt segments = [] i = 0 contour_start = None while i < len(codes): code = codes[i] if code == MOVETO: x, y = vertices[i] segments.append(('PU', int(round(x * scale)), int(round(y * scale)))) contour_start = (int(round(x * scale)), int(round(y * scale))) i += 1 elif code == LINETO: x, y = vertices[i] segments.append(('PD', int(round(x * scale)), int(round(y * scale)))) i += 1 elif code == CLOSEPOLY: if contour_start: segments.append(('PD', contour_start[0], contour_start[1])) contour_start = None i += 1 elif code == CURVE3 and i + 1 < len(vertices): for (x, y) in _flatten_quadratic( (vertices[i - 1][0], vertices[i - 1][1]), (vertices[i][0], vertices[i][1]), (vertices[i + 1][0], vertices[i + 1][1]), ): segments.append(('PD', int(round(x * scale)), int(round(y * scale)))) i += 2 elif code == CURVE4 and i + 2 < len(vertices): p0, p1, p2, p3 = vertices[i - 1], vertices[i], vertices[i + 1], vertices[i + 2] for (x, y) in _flatten_cubic(p0, p1, p2, p3): segments.append(('PD', int(round(x * scale)), int(round(y * scale)))) i += 3 else: i += 1 out = ['IN', 'SP1'] for seg in segments: if seg[0] == 'PU': out.append('PU{},{}'.format(seg[1], seg[2])) else: out.append('PD{},{}'.format(seg[1], seg[2])) out.append('PU') return ';'.join(out) + ';'