Skip to content

Commit

Permalink
Merge branch 'text2path'
Browse files Browse the repository at this point in the history
  • Loading branch information
mozman committed Feb 2, 2021
2 parents d35cc67 + e8700bf commit ec687c3
Show file tree
Hide file tree
Showing 12 changed files with 339 additions and 77 deletions.
6 changes: 4 additions & 2 deletions NEWS-v16.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ Version 0.16 - dev
from many DXF entities.
- NEW: `ezdxf.render.has_path_support()` to check if an entity is supported by
`make_path()`
- NEW: support module `disassemble`, see [docs](https://ezdxf.mozman.at/docs/disassemble.html)
- NEW: add-on `text2path`, see [docs](https://ezdxf.mozman.at/docs/addons/text2path.html)
- NEW: support module `bbox`, see [docs](https://ezdxf.mozman.at/docs/bbox.html)
- NEW: support module `disassemble`, see [docs](https://ezdxf.mozman.at/docs/disassemble.html)
- NEW: get clipping path from VIEWPORT entities by `make_path()`
- NEW: `ezdxf.math.Bezier3P`, optimized quadratic Bézier curve construction tool
- NEW: quadratic Bézier curve support for the `Path()` class
- NEW: `transform_paths()` to transform multiple `Path()` objects at once
- NEW: `path.transform_paths()` to transform multiple `Path()` objects at once
- NEW: `path.bbox()`, calculate bounding box for multiple `Path()` objects
- NEW: `path.from_matplotlib_path()` yields multiple `Path()` objects from a
matplotlib Path (TextPath) object.
- DEPRECATED: `Path.from_lwpolyline()`, replaced by factory `make_path()`
Expand Down
1 change: 1 addition & 0 deletions docs/source/addons/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ Add-ons
iterdxf
r12writer
odafc
text2path
pycsg
acadctb
forms
Expand Down
28 changes: 28 additions & 0 deletions docs/source/addons/text2path.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
.. module:: ezdxf.addons.text2path

text2path
=========

.. versionadded:: 0.16

Tools to convert text strings and text based DXF entities into outer- and inner
linear paths as :class:`~ezdxf.render.path.Path` objects. These tools depend on
the optional `matplotlib`_ package.

.. warning::

Conversion of TEXT entities into hatches does not work for spatial text not
located in the xy-plane. Contour and hole detection is done in the xy-plane
by 2D bounding boxes to be fast.

.. autofunction:: make_paths_from_str(s: str, font: FontFace, halign: int = 0, valign: int = 0, m: Matrix44 = None) -> List[Path]

.. autofunction:: make_hatches_from_str(s: str, font: FontFace, halign: int = 0, valign: int = 0, segments: int = 4, dxfattribs: Dict = None m: Matrix44 = None) -> List[Hatch]

.. autofunction:: make_paths_from_entity(entity)-> List[Path]

.. autofunction:: make_hatches_from_entity(entity) -> List[Hatch]

.. autofunction:: group_contour_and_holes(Iterable[Path]) -> Iterable[Tuple[Path, List[Path]]]

.. _matplotlib: https://matplotlib.org
2 changes: 2 additions & 0 deletions docs/source/render/path.rst
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ infinite nature.

.. autofunction:: transform_paths(paths: Iterable[Path], m: Matrix44) -> List[Path]

.. autofunction:: bbox(paths: Iterable[Path]) -> BoundingBox

.. autofunction:: from_matplotlib_path(mpath, curves=True) -> Iterable[Path]

.. class:: Path
Expand Down
47 changes: 47 additions & 0 deletions examples/addons/text_string_to_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Copyright (c) 2021, Manfred Moitzi
# License: MIT License
#
# Requires: matplotlib

from pathlib import Path
import ezdxf
from ezdxf.lldxf import const
from ezdxf.tools import fonts
from ezdxf.addons import text2path

DIR = Path('~/Desktop/Outbox').expanduser()
fonts.load()

doc = ezdxf.new()
doc.layers.new('OUTLINE')
doc.layers.new('FILLING')
msp = doc.modelspace()

attr = {'layer': 'OUTLINE', 'color': 1}
ff = fonts.FontFace(family="Source Code Pro")
s = "Source Code Pro 0123456789"
halign = const.LEFT
valign = const.BOTTOM
segments = 8

for path in text2path.make_paths_from_str(
s, ff, halign=halign, valign=valign):
# The font geometry consist of many small quadratic bezier curves.
# The distance argument for the flattening method has no big impact, but
# the segments argument is very important, which defines the minimum count
# of approximation lines for a single curve segment.
# The default value is 16 which is much to much for these
# short curve segments.
# LWPOLYLINE: this works because I know the paths are in the xy-plane,
# else an OCS transformation would be required or use add_polyline3d().
msp.add_lwpolyline(path.flattening(1, segments=segments), dxfattribs=attr)

attr['layer'] = 'FILLING'
attr['color'] = 2
for hatch in text2path.make_hatches_from_str(
s, ff, halign=halign, valign=valign,
dxfattribs=attr, segments=segments):
msp.add_entity(hatch)

doc.set_modelspace_vport(10, (7, 0))
doc.saveas(DIR / 'text2path.dxf')
45 changes: 0 additions & 45 deletions examples/text_string_to_path.py

This file was deleted.

194 changes: 171 additions & 23 deletions src/ezdxf/addons/text2path.py
Original file line number Diff line number Diff line change
@@ -1,46 +1,194 @@
# Copyright (c) 2021, Manfred Moitzi
# License: MIT License
from typing import Union, List
from typing import Union, List, Dict, Iterable, Tuple
from matplotlib.textpath import TextPath
from matplotlib.font_manager import FontProperties, findfont

from ezdxf.entities import Text, Attrib
from ezdxf.entities import Text, Attrib, Hatch
from ezdxf.lldxf import const
from ezdxf.render import Path
from ezdxf.math import Matrix44, BoundingBox
from ezdxf.render import path, nesting, Path
from ezdxf.tools import text, fonts

from matplotlib.path import Path as MPath

AnyText = Union[Text, Attrib]

# Each char consists of one or more paths!

def make_paths_from_str(s: str,
font: fonts.FontFace,
halign: int = const.LEFT,
valign: int = const.BASELINE,
m: Matrix44 = None) -> List[Path]:
""" Convert a single line string `s` into a list of
:class:`~ezdxf.render.path.Path` objects. All paths are returned in a single
list. The path objects are created for the text height of one drawing unit
as cap height (height of uppercase letter "X") and the insertion point is
(0, 0).
The paths are aligned to this insertion point.
BASELINE means the bottom of the letter "X".
def make_paths_from_entities(entity: AnyText) -> List[Path]:
""" Convert text content from DXF entities TEXT, ATTRIB and ATTDEF into a
list of :class:`~ezdxf.render.Path` objects. All paths in a single list.
Args:
s: text to convert
font: font face definition
halign: horizontal alignment: LEFT=0, CENTER=1, RIGHT=2
valign: vertical alignment: BASELINE=0, BOTTOM=1, MIDDLE=2, TOP=3
m: transformation :class:`~ezdxf.math.Matrix44`
"""
return []
font_properties, font_measurements = _get_font_data(font)
paths = _str_to_paths(s, font_properties)
bbox = path.bbox(paths, precise=False)
matrix = get_alignment_transformation(
font_measurements, bbox, halign, valign)
if m is not None:
matrix *= m
return list(path.transform_paths(paths, matrix))


def make_paths_from_str(s: str,
font: fonts.FontFace,
halign: int = const.LEFT,
valign: int = const.BASELINE) -> List[Path]:
""" Convert string `s` into a list of :class:`~ezdxf.render.Path` objects.
All paths in a single list. The path objects
are created for text height of one drawing unit as cap height (height of
uppercase letter "X") and the insertion point is (0, 0). The paths are
aligned to this insertion point. BASELINE means the bottom of the
letter "X".
def _get_font_data(
font: fonts.FontFace) -> Tuple[FontProperties, fonts.FontMeasurements]:
fp = FontProperties(
family=font.family,
style=font.style,
stretch=font.stretch,
weight=font.weight,
)
ttf_path = findfont(fp)
fonts.load() # not expensive if already loaded
# The ttf file path is the cache key for font measurements:
fm = fonts.get_font_measurements(ttf_path)
return fp, fm


def _str_to_paths(s: str, fp: FontProperties) -> List[Path]:
text_path = TextPath((0, 0), s, size=1, prop=fp, usetex=False)
return list(path.from_matplotlib_path(text_path))


def get_alignment_transformation(fm: fonts.FontMeasurements, bbox: BoundingBox,
halign: int, valign: int) -> Matrix44:
if halign == const.LEFT:
shift_x = 0
elif halign == const.RIGHT:
shift_x = -bbox.extmax.x
elif halign == const.CENTER:
shift_x = -bbox.center.x
else:
raise ValueError(f'invalid halign argument: {halign}')
cap_height = max(fm.cap_height, bbox.extmax.y)
descender_height = max(fm.descender_height, abs(bbox.extmin.y))
if valign == const.BASELINE:
shift_y = 0
elif valign == const.TOP:
shift_y = -cap_height
elif valign == const.MIDDLE:
shift_y = -cap_height / 2
elif valign == const.BOTTOM:
shift_y = descender_height
else:
raise ValueError(f'invalid valign argument: {valign}')
return Matrix44.translate(shift_x, shift_y, 0)


def group_contour_and_holes(
paths: Iterable[Path]) -> Iterable[Tuple[Path, List[Path]]]:
""" Group paths created from text strings or entities by their contour
paths. e.g. "abc" yields 3 [contour, holes] structures::
ff = fonts.FontFace(family="Arial")
paths = make_paths_from_str("abc", ff)
for contour, holes in group_contour_and_holes(paths)
for hole in holes:
# hole is a Path() object
pass
Note: These paths as easy and fast to transform,
see :func:`~ezdxf.render.path.transform_paths`
This is the basic tool to create HATCH entities from paths.
Warning: This function does not detect separated characters, e.g. "!"
creates 2 contour paths.
"""
polygons = nesting.fast_bbox_detection(paths)
for polygon in polygons:
contour = polygon[0]
if len(polygon) > 1: # are holes present?
# holes can be recursive polygons, so flatten holes:
holes = list(nesting.flatten_polygons(polygon[1]))
else:
holes = []
yield contour, holes


def make_hatches_from_str(s: str,
font: fonts.FontFace,
halign: int = const.LEFT,
valign: int = const.BASELINE,
segments: int = 4,
dxfattribs: Dict = None,
m: Matrix44 = None) -> List[Hatch]:
""" Convert a single line string `s` into a list of virtual
:class:`~ezdxf.entities.Hatch` entities.
The path objects are created for the text height of one drawing unit as cap
height (height of uppercase letter "X") and the insertion point is (0, 0).
The HATCH entities are aligned to this insertion point. BASELINE means the
bottom of the letter "X".
Args:
s: text to convert
font: font face definition
halign: horizontal alignment: LEFT=0, Center=1, RIGHT=2
halign: horizontal alignment: LEFT=0, CENTER=1, RIGHT=2
valign: vertical alignment: BASELINE=0, BOTTOM=1, MIDDLE=2, TOP=3
segments: minimal segment count per Bézier curve
dxfattribs: additional DXF attributes
m: transformation :class:`~ezdxf.math.Matrix44`
"""
font_properties, font_measurements = _get_font_data(font)
paths = _str_to_paths(s, font_properties)

# HATCH is an OCS entity, transforming just the polyline paths
# is not correct! The Hatch has to be created in the xy-plane!
hatches = []
dxfattribs = dxfattribs or dict()
dxfattribs.setdefault('solid_fill', 1)
dxfattribs.setdefault('pattern_name', 'SOLID')
dxfattribs.setdefault('color', 7)

for contour, holes in group_contour_and_holes(paths):
hatch = Hatch.new(dxfattribs=dxfattribs)
hatch.paths.add_polyline_path(
contour.flattening(1, segments=segments), flags=1) # 1=external
for hole in holes:
hatch.paths.add_polyline_path(
hole.flattening(1, segments=segments), flags=0) # 0=normal
hatches.append(hatch)

bbox = path.bbox(paths, precise=False)
matrix = get_alignment_transformation(
font_measurements, bbox, halign, valign)
if m is not None:
matrix *= m

# Transform HATCH entities as a unit:
return [hatch.transform(matrix) for hatch in hatches]


def make_paths_from_entity(entity: AnyText) -> List[Path]:
""" Convert text content from DXF entities TEXT, ATTRIB and ATTDEF into a
list of :class:`~ezdxf.render.Path` objects. All paths are returned in a
single list.
The paths are located at the location of the source entity, but don't expect
a 100% match compared to CAD applications.
"""
return []


def make_hatches_from_entity(entity: AnyText) -> List[Hatch]:
""" Convert text content from DXF entities TEXT, ATTRIB and ATTDEF into a
list of virtual :class:`~ezdxf.entities.Hatch` entities.
The hatches are located at the location of the source entity, but don't
expect a 100% match compared to CAD applications.
"""
return []
5 changes: 2 additions & 3 deletions src/ezdxf/render/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# Created: 28.12.2018
# Copyright (c) 2018-2020, Manfred Moitzi
# Copyright (c) 2018-2021, Manfred Moitzi
# License: MIT License
from .arrows import ARROWS
from .r12spline import R12Spline
from .curves import Bezier, EulerSpiral, Spline, random_2d_path, random_3d_path
from .mesh import MeshBuilder, MeshVertexMerger, MeshTransformer, MeshAverageVertexMerger
from .trace import TraceBuilder
from .path import Path, Command, has_path_support, make_path
from .path import Path, Command, has_path_support, make_path, from_matplotlib_path
Loading

0 comments on commit ec687c3

Please sign in to comment.