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:
jan 2016-10-19 16:52:28 -07:00
parent 3e1ff19270
commit 3a460a9296
4 changed files with 204 additions and 58 deletions

View File

@ -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:

View File

@ -9,4 +9,4 @@ from .polygon import Polygon
from .circle import Circle
from .ellipse import Ellipse
from .arc import Arc
from .text import Text

View File

@ -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

View File

@ -16,6 +16,7 @@ setup(name='masque',
'visualization': ['matplotlib'],
'gdsii': ['python-gdsii'],
'svg': ['svgwrite'],
'text': ['freetype-py', 'matplotlib']
},
)