Files
stepanalyser/.venv/lib/python3.12/site-packages/ezdxf/addons/mtxpl.py
Christian Anetzberger a197de9456 initial
2026-01-22 20:23:51 +01:00

389 lines
13 KiB
Python

# Copyright (c) 2021-2022, Manfred Moitzi
# License: MIT License
from __future__ import annotations
from typing import cast, Any, Optional, TYPE_CHECKING
import math
import ezdxf
from ezdxf.entities import MText, DXFGraphic, Textstyle
from ezdxf.enums import TextEntityAlignment
# from ezdxf.layouts import BaseLayout
from ezdxf.document import Drawing
from ezdxf.math import Matrix44
from ezdxf.fonts import fonts
from ezdxf.tools import text_layout as tl
from ezdxf.tools.text import MTextContext
from ezdxf.render.abstract_mtext_renderer import AbstractMTextRenderer
if TYPE_CHECKING:
from ezdxf.eztypes import GenericLayoutType
__all__ = ["MTextExplode"]
class FrameRenderer(tl.ContentRenderer):
def __init__(self, attribs: dict, layout: GenericLayoutType):
self.line_attribs = attribs
self.layout = layout
def render(
self,
left: float,
bottom: float,
right: float,
top: float,
m: Matrix44 = None,
) -> None:
pline = self.layout.add_lwpolyline(
[(left, top), (right, top), (right, bottom), (left, bottom)],
close=True,
dxfattribs=self.line_attribs,
)
if m:
pline.transform(m)
def line(
self, x1: float, y1: float, x2: float, y2: float, m: Matrix44 = None
) -> None:
line = self.layout.add_line((x1, y1), (x2, y2), dxfattribs=self.line_attribs)
if m:
line.transform(m)
class ColumnBackgroundRenderer(FrameRenderer):
def __init__(
self,
attribs: dict,
layout: GenericLayoutType,
bg_aci: Optional[int] = None,
bg_true_color: Optional[int] = None,
offset: float = 0,
text_frame: bool = False,
):
super().__init__(attribs, layout)
self.solid_attribs = None
if bg_aci is not None:
self.solid_attribs = dict(attribs)
self.solid_attribs["color"] = bg_aci
elif bg_true_color is not None:
self.solid_attribs = dict(attribs)
self.solid_attribs["true_color"] = bg_true_color
self.offset = offset # background border offset
self.has_text_frame = text_frame
def render(
self,
left: float,
bottom: float,
right: float,
top: float,
m: Optional[Matrix44] = None,
) -> None:
# Important: this is not a clipping box, it is possible to
# render anything outside of the given borders!
offset = self.offset
left -= offset
right += offset
top += offset
bottom -= offset
if self.solid_attribs is not None:
solid = self.layout.add_solid(
# SOLID! swap last two vertices:
[(left, top), (right, top), (left, bottom), (right, bottom)],
dxfattribs=self.solid_attribs,
)
if m:
solid.transform(m)
if self.has_text_frame:
super().render(left, bottom, right, top, m)
class TextRenderer(FrameRenderer):
"""Text content renderer."""
def __init__(
self,
text: str,
text_attribs: dict,
line_attribs: dict,
layout: GenericLayoutType,
):
super().__init__(line_attribs, layout)
self.text = text
self.text_attribs = text_attribs
def render(
self,
left: float,
bottom: float,
right: float,
top: float,
m: Optional[Matrix44] = None,
):
"""Create/render the text content"""
text = self.layout.add_text(self.text, dxfattribs=self.text_attribs)
text.set_placement((left, bottom), align=TextEntityAlignment.LEFT)
if m:
text.transform(m)
# todo: replace by fonts.get_entity_font_face()
def get_font_face(entity: DXFGraphic, doc=None) -> fonts.FontFace:
"""Returns the :class:`~ezdxf.tools.fonts.FontFace` defined by the
associated text style. Returns the default font face if the `entity` does
not have or support the DXF attribute "style".
Pass a DXF document as argument `doc` to resolve text styles for virtual
entities which are not assigned to a DXF document. The argument `doc`
always overrides the DXF document to which the `entity` is assigned to.
"""
if entity.doc and doc is None:
doc = entity.doc
assert doc is not None, "valid DXF document required"
style_name = ""
# This works also for entities which do not support "style",
# where style_name = entity.dxf.get("style") would fail.
if entity.dxf.is_supported("style"):
style_name = entity.dxf.style
font_face = fonts.FontFace()
if style_name and doc is not None:
style = cast(Textstyle, doc.styles.get(style_name))
family, italic, bold = style.get_extended_font_data()
if family:
text_style = "Italic" if italic else "Regular"
text_weight = 700 if bold else 400
font_face = fonts.FontFace(
family=family, style=text_style, weight=text_weight
)
else:
ttf = style.dxf.font
if ttf:
font_face = fonts.get_font_face(ttf)
return font_face
def get_color_attribs(ctx: MTextContext) -> dict:
attribs = {"color": ctx.aci}
if ctx.rgb is not None:
attribs["true_color"] = ezdxf.rgb2int(ctx.rgb)
return attribs
def make_bg_renderer(mtext: MText, layout: GenericLayoutType):
attribs = get_base_attribs(mtext)
dxf = mtext.dxf
bg_fill = dxf.get("bg_fill", 0)
bg_aci = None
bg_true_color = None
has_text_frame = False
offset = 0
if bg_fill:
# The fill scale is a multiple of the initial char height and
# a scale of 1, fits exact the outer border
# of the column -> offset = 0
offset = dxf.char_height * (dxf.get("box_fill_scale", 1.5) - 1)
if bg_fill & ezdxf.const.MTEXT_BG_COLOR:
if dxf.hasattr("bg_fill_color"):
bg_aci = dxf.bg_fill_color
if dxf.hasattr("bg_fill_true_color"):
bg_aci = None
bg_true_color = dxf.bg_fill_true_color
if (bg_fill & 3) == 3: # canvas color = bit 0 and 1 set
# can not detect canvas color from DXF document!
# do not draw any background:
bg_aci = None
bg_true_color = None
if bg_fill & ezdxf.const.MTEXT_TEXT_FRAME:
has_text_frame = True
return ColumnBackgroundRenderer(
attribs,
layout,
bg_aci=bg_aci,
bg_true_color=bg_true_color,
offset=offset,
text_frame=has_text_frame,
)
def get_base_attribs(mtext: MText) -> dict:
dxf = mtext.dxf
attribs = {
"layer": dxf.layer,
"color": dxf.color,
}
return attribs
class MTextExplode(AbstractMTextRenderer):
"""The :class:`MTextExplode` class is a tool to disassemble MTEXT entities
into single line TEXT entities and additional LINE entities if required to
emulate strokes.
The `layout` argument defines the target layout for "exploded" parts of the
MTEXT entity. Use argument `doc` if the target layout has no DXF document assigned
like virtual layouts. The `spacing_factor` argument is an advanced tuning parameter
to scale the size of space chars.
"""
def __init__(
self,
layout: GenericLayoutType,
doc: Optional[Drawing] = None,
spacing_factor: float = 1.0,
):
super().__init__()
self.layout: GenericLayoutType = layout
self._doc = doc
# scale the width of spaces by this factor:
self._spacing_factor = float(spacing_factor)
self._required_text_styles: dict[str, fonts.FontFace] = {}
self.current_base_attribs: dict[str, Any] = dict()
# Implementation of required AbstractMTextRenderer methods and overrides:
def layout_engine(self, mtext: MText) -> tl.Layout:
self.current_base_attribs = get_base_attribs(mtext)
return super().layout_engine(mtext)
def word(self, text: str, ctx: MTextContext) -> tl.ContentCell:
line_attribs = dict(self.current_base_attribs or {})
line_attribs.update(get_color_attribs(ctx))
text_attribs = dict(line_attribs)
text_attribs.update(self.get_text_attribs(ctx))
return tl.Text(
width=self.get_font(ctx).text_width(text),
height=ctx.cap_height,
valign=tl.CellAlignment(ctx.align),
stroke=self.get_stroke(ctx),
renderer=TextRenderer(text, text_attribs, line_attribs, self.layout),
)
def fraction(self, data: tuple, ctx: MTextContext) -> tl.ContentCell:
upr, lwr, type_ = data
if type_:
return tl.Fraction(
top=self.word(upr, ctx),
bottom=self.word(lwr, ctx),
stacking=self.get_stacking(type_),
# renders just the divider line:
renderer=FrameRenderer(self.current_base_attribs, self.layout),
)
else:
return self.word(upr, ctx)
def get_font_face(self, mtext: MText) -> fonts.FontFace:
return get_font_face(mtext)
def make_bg_renderer(self, mtext: MText) -> tl.ContentRenderer:
return make_bg_renderer(mtext, self.layout)
# Implementation details of MTextExplode:
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.finalize()
def mtext_exploded_text_style(self, font_face: fonts.FontFace) -> str:
style = 0
if font_face.is_bold:
style += 1
if font_face.is_italic:
style += 2
style_str = str(style) if style > 0 else ""
# BricsCAD naming convention for exploded MTEXT styles:
text_style = f"MtXpl_{font_face.family}" + style_str
self._required_text_styles[text_style] = font_face
return text_style
def get_font(self, ctx: MTextContext) -> fonts.AbstractFont:
ttf = fonts.find_font_file_name(ctx.font_face)
key = (ttf, ctx.cap_height, ctx.width_factor)
font = self._font_cache.get(key)
if font is None:
font = fonts.make_font(ttf, ctx.cap_height, ctx.width_factor)
self._font_cache[key] = font
return font
def get_text_attribs(self, ctx: MTextContext) -> dict:
attribs = {
"height": ctx.cap_height,
"style": self.mtext_exploded_text_style(ctx.font_face),
}
if not math.isclose(ctx.width_factor, 1.0):
attribs["width"] = ctx.width_factor
if abs(ctx.oblique) > 1e-6:
attribs["oblique"] = ctx.oblique
return attribs
def explode(self, mtext: MText, destroy=True):
"""Explode `mtext` and destroy the source entity if argument `destroy`
is ``True``.
"""
align = tl.LayoutAlignment(mtext.dxf.attachment_point)
layout_engine = self.layout_engine(mtext)
layout_engine.place(align=align)
layout_engine.render(mtext.ucs().matrix)
if destroy:
mtext.destroy()
def finalize(self):
"""Create required text styles. This method is called automatically if
the class is used as context manager. This method does not work with virtual
layouts if no document was assigned at initialization!
"""
doc = self._doc
if doc is None:
doc = self.layout.doc
if doc is None:
raise ezdxf.DXFValueError(
"DXF document required, finalize() does not work with virtual layouts "
"if no document was assigned at initialization."
)
text_styles = doc.styles
for style in self.make_required_style_table_entries():
try:
text_styles.add_entry(style)
except ezdxf.DXFTableEntryError:
pass
def make_required_style_table_entries(self) -> list[Textstyle]:
def ttf_path(font_face: fonts.FontFace) -> str:
ttf = font_face.filename
if not ttf:
ttf = fonts.find_font_file_name(font_face)
else:
# remapping SHX replacement fonts to SHX fonts,
# like "txt_____.ttf" to "TXT.SHX":
shx = fonts.map_ttf_to_shx(ttf)
if shx:
ttf = shx
return ttf
text_styles: list[Textstyle] = []
for name, font_face in self._required_text_styles.items():
ttf = ttf_path(font_face)
style = Textstyle.new(dxfattribs={
"name": name,
"font": ttf,
})
if not ttf.endswith(".SHX"):
style.set_extended_font_data(
font_face.family,
italic=font_face.is_italic,
bold=font_face.is_bold,
)
text_styles.append(style)
return text_styles