Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add fonts #743

Merged
merged 7 commits into from Sep 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGELOG.md
Expand Up @@ -3,9 +3,10 @@
## 5.36.0

- Add thermal solver [PR](https://github.com/gdsfactory/gdsfactory/pull/739)
- remove phidl dependency
- remove phidl dependency [PR](https://github.com/gdsfactory/gdsfactory/pull/741)
- remove incremental naming from phidl
- remove Port.midpoint as it was deprecated since 5.14.0
- add freetype-py for using text with font and add components.text_freetype

## 5.35.0

Expand Down
1 change: 1 addition & 0 deletions construct.yaml
Expand Up @@ -12,6 +12,7 @@ specs:
- mamba
- pip
- gdsfactory
- freetype-py
- spyder
- jupyterlab
- networkx
Expand Down
15 changes: 15 additions & 0 deletions docs/components.rst
Expand Up @@ -2915,6 +2915,21 @@ text



text_freetype
----------------------------------------------------

.. autofunction:: gdsfactory.components.text_freetype

.. plot::
:include-source:

import gdsfactory as gf

c = gf.components.text_freetype(text='abcd', size=10, justify='left', layer='WG', font='Arial')
c.plot()



text_lines
----------------------------------------------------

Expand Down
2 changes: 2 additions & 0 deletions gdsfactory/components/__init__.py
Expand Up @@ -238,6 +238,7 @@
)
from gdsfactory.components.taper_parabolic import taper_parabolic
from gdsfactory.components.text import text, text_lines
from gdsfactory.components.text_freetype import text_freetype
from gdsfactory.components.text_rectangular import (
text_rectangular,
text_rectangular_multi_layer,
Expand Down Expand Up @@ -496,6 +497,7 @@
"taper_w11_l200",
"taper_w12_l200",
"text",
"text_freetype",
"text_rectangular",
"text_rectangular_multi_layer",
"triangle",
Expand Down
118 changes: 118 additions & 0 deletions gdsfactory/components/text_freetype.py
@@ -0,0 +1,118 @@
import os
import warnings

import numpy as np

import gdsfactory as gf
from gdsfactory.component import Component
from gdsfactory.constants import _glyph, _indent, _width
from gdsfactory.types import LayerSpec


@gf.cell
def text_freetype(
text: str = "abcd",
size: int = 10,
justify: str = "left",
layer: LayerSpec = "WG",
font: str = "DEPLOF",
) -> Component:
"""Returns text Component.

Args:
text: string.
size: in um.
position: x, y position.
justify: left, right, center.
layer: for the text.
font: Font face to use. Default DEPLOF does not require additional libraries,
otherwise freetype load fonts. You can choose font by name
(e.g. "Times New Roman"), or by file OTF or TTF filepath.
"""
t = Component()
xoffset = 0
yoffset = 0

face = font
if face == "DEPLOF":
scaling = size / 1000

for line in text.split("\n"):
char = Component()
for c in line:
ascii_val = ord(c)
if c == " ":
xoffset += 500 * scaling
elif (33 <= ascii_val <= 126) or (ascii_val == 181):
for poly in _glyph[ascii_val]:
xpts = np.array(poly)[:, 0] * scaling
ypts = np.array(poly)[:, 1] * scaling
char.add_polygon([xpts + xoffset, ypts + yoffset], layer=layer)
xoffset += (_width[ascii_val] + _indent[ascii_val]) * scaling
else:
valid_chars = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~µ"
warnings.warn(
'text(): Warning, some characters ignored, no geometry for character "%s" with ascii value %s. '
"Valid characters: %s"
% (chr(ascii_val), ascii_val, valid_chars)
)
t.add_ref(char)
yoffset -= 1500 * scaling
xoffset = 0
else:
from gdsfactory.font import _get_font_by_file, _get_font_by_name, _get_glyph

# Load the font
# If we've passed a valid file, try to load that, otherwise search system fonts
font = None
if (face.endswith(".otf") or face.endswith(".ttf")) and os.path.exists(face):
font = _get_font_by_file(face)
else:
try:
font = _get_font_by_name(face)
except ValueError:
pass
if font is None:
raise ValueError(
f"Failed to find font: {face!r}. "
"Try specifying the exact (full) path to the .ttf or .otf file. "
)

# Render each character
for line in text.split("\n"):
char = Component()
xoffset = 0
for letter in line:
letter_dev = Component()
letter_template, advance_x = _get_glyph(font, letter)
for poly in letter_template.polygons:
letter_dev.add_polygon(poly.polygons, layer=layer)
ref = char.add_ref(letter_dev)
ref.move(destination=(xoffset, 0))
ref.magnification = size
xoffset += size * advance_x

ref = t.add_ref(char)
ref.move(destination=(0, yoffset))
yoffset -= size
t.absorb(ref)

justify = justify.lower()
for ref in t.references:
if justify == "left":
pass
if justify == "right":
ref.xmax = 0
if justify == "center":
ref.move(origin=ref.center, destination=(0, 0), axis="x")

t.flatten()
return t


if __name__ == "__main__":
c2 = text_freetype(
"hello",
# font="Times New Roman"
)
c2.show(show_ports=True)
213 changes: 213 additions & 0 deletions gdsfactory/font.py
@@ -0,0 +1,213 @@
""" Support for font rendering in GDS files."""


import gdspy
import numpy as np
from matplotlib import font_manager

from gdsfactory.component import Component

_cached_fonts = {}

try:
import freetype
except ImportError as e:
raise ImportError(
"gdsfactory requires freetype to use real fonts. "
"Either use the default DEPLOF font or install the freetype package:"
"\n\n $ pip install freetype-py"
"\n\n (Note: Windows users may have to find and replace the 'libfreetype.dll' "
"file in their Python package directory /freetype/ with the correct one"
"from here: https://github.com/ubawurinna/freetype-windows-binaries"
" -- be sure to rename 'freetype.dll' to 'libfreetype.dll') "
) from e


def _get_font_by_file(file):
"""
Load a given font file.

Args:
file [str, BinaryIO]: Load a font face from a given file
"""
# Cache opened fonts
if file in _cached_fonts:
return _cached_fonts[file]

font_renderer = freetype.Face(file)
font_renderer.set_char_size(32 * 64) # 32pt size
_cached_fonts[file] = font_renderer
return font_renderer


def _get_font_by_name(name):
"""
Try and load a system font by name.

Args:
name [str]: Load a system font
"""
try:
font_file = font_manager.findfont(name, fallback_to_default=False)
except Exception as e:
raise ValueError(
f"Failed to find font: {name!r}"
"Try specifying the exact (full) path to the .ttf or .otf file. "
"Otherwise, it might be resolved by rebuilding the matplotlib font cache"
) from e
return _get_font_by_file(font_file)


def _get_glyph(font, letter): # noqa: C901
"""
Get a block reference to the given letter
"""
if not isinstance(letter, str) and len(letter) == 1:
raise TypeError(f"Letter must be a string of length 1. Got: {letter!r}")

if not isinstance(font, freetype.Face):
raise TypeError(
"font {font!r} must be a freetype font face. "
"Load a font using _get_font_by_name first."
)

if getattr(font, "gds_glyphs", None) is None:
font.gds_glyphs = {}

if letter in font.gds_glyphs:
return font.gds_glyphs[letter]

# Get the font name
font_name = font.get_sfnt_name(
freetype.TT_NAME_IDS["TT_NAME_ID_PS_NAME"]
).string.decode()
if not font_name:
# If there is no postscript name, use the family name
font_name = font.family_name.replace(" ", "_")

block_name = f"*char_{font_name}_0x{ord(letter):2X}"

# Load control points from font file
font.load_char(letter, freetype.FT_LOAD_FLAGS["FT_LOAD_NO_BITMAP"])
glyph = font.glyph
outline = glyph.outline
points = np.array(outline.points, dtype=float) / font.size.ascender
tags = outline.tags

# Add polylines
start, end = 0, -1
polylines = []
for contour in outline.contours:
start = end + 1
end = contour

# Build up the letter as a curve
cpoint = start
curve = gdspy.Curve(*points[cpoint], tolerance=0.001)
while cpoint <= end:
# Figure out what sort of point we are looking at
if tags[cpoint] & 1:
# We are at an on-curve control point. The next point may be
# another on-curve point, in which case we create a straight
# line interpolation, or it may be a quadratic or cubic
# bezier curve. But first we check if we are at the end of the array
if cpoint == end:
ntag = tags[start]
npoint = points[start]
else:
ntag = tags[cpoint + 1]
npoint = points[cpoint + 1]

# Then add the control points
if ntag & 1:
curve.L(*npoint)
cpoint += 1
elif ntag & 2:
# We are at a cubic bezier curve point
if cpoint + 3 <= end:
curve.C(*points[cpoint + 1 : cpoint + 4].flatten())
elif cpoint + 2 <= end:
plist = list(points[cpoint + 1 : cpoint + 3].flatten())
plist.extend(points[start])
curve.C(*plist)
else:
raise ValueError(
"Missing bezier control points. We require at least"
" two control points to get a cubic curve."
)
cpoint += 3
else:
# Otherwise we're at a quadratic bezier curve point
if cpoint + 2 > end:
cpoint_2 = start
end_tag = tags[start]
else:
cpoint_2 = cpoint + 2
end_tag = tags[cpoint_2]
p1 = points[cpoint + 1]
p2 = points[cpoint_2]

# Check if we are at a sequential control point. In that case,
# p2 is actually the midpoint of p1 and p2.
if end_tag & 1 == 0:
p2 = (p1 + p2) / 2

# Add the curve
curve.Q(p1[0], p1[1], p2[0], p2[1])
cpoint += 2
else:
# We are looking at a control point
if not tags[cpoint] & 2:
# We are at a quadratic sequential control point.
# Check if we're at the end of the segment
if cpoint == end:
cpoint_1 = start
end_tag = tags[start]
else:
cpoint_1 = cpoint + 1
end_tag = tags[cpoint_1]

# If we are at the beginning, this is a special case,
# we need to reset the starting position
if cpoint == start:
p0 = points[end]
p1 = points[cpoint]
p2 = points[cpoint_1]
if tags[end] & 1 == 0:
# If the last point is also a control point, then the end is actually
# halfway between here and the last point
p0 = (p0 + p1) / 2
# And reset the starting position of the spline
curve = gdspy.Curve(*p0, tolerance=0.001)
else:
# The first control point is at the midpoint of this control point and the
# previous control point
p0 = points[cpoint - 1]
p1 = points[cpoint]
p2 = points[cpoint_1]
p0 = (p0 + p1) / 2

# Check if we are at a sequential control point again
if end_tag & 1 == 0:
p2 = (p1 + p2) / 2

# And add the segment
curve.Q(p1[0], p1[1], p2[0], p2[1])
cpoint += 1
else:
raise ValueError(
"Sequential control points not valid for cubic splines."
)
polylines.append(gdspy.Polygon(curve.get_points()))

# Construct the component
component = Component(block_name)
if polylines:
letter_polyline = polylines[0]
for polyline in polylines[1:]:
letter_polyline = gdspy.boolean(letter_polyline, polyline, "xor")
component.add_polygon(letter_polyline)

# Cache the return value and return it
font.gds_glyphs[letter] = (component, glyph.advance.x / font.size.ascender)
return font.gds_glyphs[letter]