Add Text shape
Rendered using freetype-py and matplotlib Can eliminate the matplotlib dependency if I write my own bezier code, but that's work (and I already use matplotlib...).
This commit is contained in:
parent
3e1ff19270
commit
3a460a9296
@ -11,9 +11,10 @@ E-beam doses, and the ability to output to multiple formats.
|
||||
Requirements:
|
||||
* python 3 (written and tested with 3.5)
|
||||
* numpy
|
||||
* matplotlib (optional, used for visualization functions)
|
||||
* matplotlib (optional, used for visualization functions and text)
|
||||
* python-gdsii (optional, used for gdsii i/o)
|
||||
* svgwrite (optional, used for svg output)
|
||||
* freetype (optional, used for text)
|
||||
|
||||
|
||||
Install with pip, via git:
|
||||
|
@ -9,4 +9,4 @@ from .polygon import Polygon
|
||||
from .circle import Circle
|
||||
from .ellipse import Ellipse
|
||||
from .arc import Arc
|
||||
|
||||
from .text import Text
|
||||
|
@ -1,57 +1,201 @@
|
||||
from typing import List, Tuple
|
||||
import numpy
|
||||
from numpy import pi, inf
|
||||
|
||||
#
|
||||
# class Text(Shape):
|
||||
# _string = ''
|
||||
# _height = 1.0
|
||||
# _rotation = 0.0
|
||||
# font_path = ''
|
||||
#
|
||||
# # vertices property
|
||||
# @property
|
||||
# def string(self):
|
||||
# return self._string
|
||||
#
|
||||
# @string.setter
|
||||
# def string(self, val):
|
||||
# self._string = val
|
||||
#
|
||||
# # Rotation property
|
||||
# @property
|
||||
# def rotation(self):
|
||||
# return self._rotation
|
||||
#
|
||||
# @rotation.setter
|
||||
# def rotation(self, val):
|
||||
# if not is_scalar(val):
|
||||
# raise PatternError('Rotation must be a scalar')
|
||||
# self._rotation = val % (2 * pi)
|
||||
#
|
||||
# # Height property
|
||||
# @property
|
||||
# def height(self):
|
||||
# return self._height
|
||||
#
|
||||
# @height.setter
|
||||
# def height(self, val):
|
||||
# if not is_scalar(val):
|
||||
# raise PatternError('Height must be a scalar')
|
||||
# self._height = val
|
||||
#
|
||||
# def __init__(self, text, height, font_path, rotation=0.0, offset=(0.0, 0.0), layer=0, dose=1.0):
|
||||
# self.offset = offset
|
||||
# self.layer = layer
|
||||
# self.dose = dose
|
||||
# self.text = text
|
||||
# self.height = height
|
||||
# self.rotation = rotation
|
||||
# self.font_path = font_path
|
||||
#
|
||||
# def to_polygon(self, _poly_num_points=None, _poly_max_arclen=None):
|
||||
#
|
||||
# return copy.deepcopy(self)
|
||||
#
|
||||
# def rotate(self, theta):
|
||||
# self.rotation += theta
|
||||
#
|
||||
# def scale_by(self, c):
|
||||
# self.height *= c
|
||||
from . import Shape, Polygon, normalized_shape_tuple
|
||||
from .. import PatternError
|
||||
from ..utils import is_scalar, vector2, get_bit
|
||||
|
||||
# Loaded on use:
|
||||
# from freetype import Face
|
||||
# from matplotlib.path import Path
|
||||
|
||||
|
||||
__author__ = 'Jan Petykiewicz'
|
||||
|
||||
|
||||
class Text(Shape):
|
||||
_string = ''
|
||||
_height = 1.0
|
||||
_rotation = 0.0
|
||||
font_path = ''
|
||||
|
||||
# vertices property
|
||||
@property
|
||||
def string(self) -> str:
|
||||
return self._string
|
||||
|
||||
@string.setter
|
||||
def string(self, val: str):
|
||||
self._string = val
|
||||
|
||||
# Rotation property
|
||||
@property
|
||||
def rotation(self) -> float:
|
||||
return self._rotation
|
||||
|
||||
@rotation.setter
|
||||
def rotation(self, val: float):
|
||||
if not is_scalar(val):
|
||||
raise PatternError('Rotation must be a scalar')
|
||||
self._rotation = val % (2 * pi)
|
||||
|
||||
# Height property
|
||||
@property
|
||||
def height(self) -> float:
|
||||
return self._height
|
||||
|
||||
@height.setter
|
||||
def height(self, val: float):
|
||||
if not is_scalar(val):
|
||||
raise PatternError('Height must be a scalar')
|
||||
self._height = val
|
||||
|
||||
def __init__(self,
|
||||
string: str,
|
||||
height: float,
|
||||
font_path: str,
|
||||
rotation: float=0.0,
|
||||
offset: vector2=(0.0, 0.0),
|
||||
layer: int=0,
|
||||
dose: float=1.0):
|
||||
self.offset = offset
|
||||
self.layer = layer
|
||||
self.dose = dose
|
||||
self.string = string
|
||||
self.height = height
|
||||
self.rotation = rotation
|
||||
self.font_path = font_path
|
||||
|
||||
def to_polygons(self,
|
||||
_poly_num_points: int=None,
|
||||
_poly_max_arclen: float=None
|
||||
) -> List[Polygon]:
|
||||
all_polygons = []
|
||||
total_advance = 0
|
||||
for char in self.string:
|
||||
raw_polys, advance = get_char_as_polygons(self.font_path, char)
|
||||
|
||||
# Move these polygons to the right of the previous letter
|
||||
for xys in raw_polys:
|
||||
poly = Polygon(xys, dose=self.dose, layer=self.layer)
|
||||
poly.scale_by(self.height)
|
||||
poly.offset = self.offset + [total_advance, 0]
|
||||
# poly.scale_by(self.height)
|
||||
poly.rotate_around(self.offset, self.rotation)
|
||||
all_polygons += [poly]
|
||||
|
||||
# Update the list of all polygons and how far to advance
|
||||
total_advance += advance * self.height
|
||||
|
||||
return all_polygons
|
||||
|
||||
def rotate(self, theta: float):
|
||||
self.rotation += theta
|
||||
|
||||
def scale_by(self, c: float):
|
||||
self.height *= c
|
||||
|
||||
def normalized_form(self, norm_value: float) -> normalized_shape_tuple:
|
||||
return (type(self), self.string, self.font_path, self.layer), \
|
||||
(self.offset, self.height / norm_value, self.rotation, self.dose), \
|
||||
lambda: Text(string=self.string,
|
||||
height=self.height * norm_value,
|
||||
font_path=self.font_path,
|
||||
layer=self.layer)
|
||||
|
||||
def get_bounds(self) -> numpy.ndarray:
|
||||
# rotation makes this a huge pain when using slot.advance and glyph.bbox(), so
|
||||
# just convert to polygons instead
|
||||
bounds = [[+inf, +inf], [-inf, -inf]]
|
||||
polys = self.to_polygons()
|
||||
for poly in polys:
|
||||
poly_bounds = poly.get_bounds()
|
||||
bounds[0, :] = numpy.minimum(bounds[0, :], poly_bounds[0, :])
|
||||
bounds[1, :] = numpy.maximum(bounds[1, :], poly_bounds[1, :])
|
||||
|
||||
return bounds
|
||||
|
||||
|
||||
def get_char_as_polygons(font_path: str,
|
||||
char: str,
|
||||
resolution: float=48*64,
|
||||
) -> Tuple[List[List[List[float]]], float]:
|
||||
from freetype import Face
|
||||
from matplotlib.path import Path
|
||||
|
||||
"""
|
||||
Get a list of polygons representing a single character.
|
||||
|
||||
The output is normalized so that the font size is 1 unit.
|
||||
|
||||
:param font_path: File path specifying a font loadable by freetype
|
||||
:param char: Character to convert to polygons
|
||||
:param resolution: Internal resolution setting (used for freetype
|
||||
Face.set_font_size(resolution)). Modify at your own peril!
|
||||
:return: List of polygons [[[x0, y0], [x1, y1], ...], ...] and 'advance' distance (distance
|
||||
from the start of this glyph to the start of the next one)
|
||||
"""
|
||||
if len(char) != 1:
|
||||
raise Exception('get_char_as_polygons called with non-char')
|
||||
|
||||
face = Face(font_path)
|
||||
face.set_char_size(resolution)
|
||||
face.load_char(char)
|
||||
slot = face.glyph
|
||||
outline = slot.outline
|
||||
|
||||
start = 0
|
||||
all_verts, all_codes = [], []
|
||||
for end in outline.contours:
|
||||
points = outline.points[start:end + 1]
|
||||
points.append(points[0])
|
||||
|
||||
tags = outline.tags[start:end + 1]
|
||||
tags.append(tags[0])
|
||||
|
||||
segments = []
|
||||
for j, point in enumerate(points):
|
||||
# If we already have a segment, add this point to it
|
||||
if j > 0:
|
||||
segments[-1].append(point)
|
||||
|
||||
# If not bezier control point, start next segment
|
||||
if get_bit(tags[j], 0) and j < (len(points) - 1):
|
||||
segments.append([point])
|
||||
|
||||
verts = [points[0]]
|
||||
codes = [Path.MOVETO]
|
||||
for segment in segments:
|
||||
if len(segment) == 2:
|
||||
verts.extend(segment[1:])
|
||||
codes.extend([Path.LINETO])
|
||||
elif len(segment) == 3:
|
||||
verts.extend(segment[1:])
|
||||
codes.extend([Path.CURVE3, Path.CURVE3])
|
||||
else:
|
||||
verts.append(segment[1])
|
||||
codes.append(Path.CURVE3)
|
||||
for i in range(1, len(segment) - 2):
|
||||
a, b = segment[i], segment[i + 1]
|
||||
c = ((a[0] + b[0]) / 2.0, (a[1] + b[1]) / 2.0)
|
||||
verts.extend([c, b])
|
||||
codes.extend([Path.CURVE3, Path.CURVE3])
|
||||
verts.append(segment[-1])
|
||||
codes.append(Path.CURVE3)
|
||||
all_verts.extend(verts)
|
||||
all_codes.extend(codes)
|
||||
start = end + 1
|
||||
|
||||
all_verts = numpy.array(all_verts) / resolution
|
||||
|
||||
advance = slot.advance.x / resolution
|
||||
|
||||
if len(all_verts) == 0:
|
||||
polygons = []
|
||||
else:
|
||||
path = Path(all_verts, all_codes)
|
||||
path.should_simplify = False
|
||||
polygons = path.to_polygons()
|
||||
|
||||
return polygons, advance
|
||||
|
Loading…
Reference in New Issue
Block a user