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

297 lines
9.6 KiB
Python

# Copyright (c) 2021-2023, Manfred Moitzi
# License: MIT License
# This is the abstract link between the text layout engine implemented in
# ezdxf.tools.text_layout and a concrete MTEXT renderer implementation like
# MTextExplode or ComplexMTextRenderer.
from __future__ import annotations
from typing import Sequence, Optional
import abc
from ezdxf.lldxf import const
from ezdxf import colors
from ezdxf.entities.mtext import MText, MTextColumns
from ezdxf.enums import (
MTextParagraphAlignment,
)
from ezdxf.fonts import fonts
from ezdxf.tools import text_layout as tl
from ezdxf.tools.text import (
MTextParser,
MTextContext,
TokenType,
ParagraphProperties,
estimate_mtext_extents,
)
__all__ = ["AbstractMTextRenderer"]
ALIGN = {
MTextParagraphAlignment.LEFT: tl.ParagraphAlignment.LEFT,
MTextParagraphAlignment.RIGHT: tl.ParagraphAlignment.RIGHT,
MTextParagraphAlignment.CENTER: tl.ParagraphAlignment.CENTER,
MTextParagraphAlignment.JUSTIFIED: tl.ParagraphAlignment.JUSTIFIED,
MTextParagraphAlignment.DISTRIBUTED: tl.ParagraphAlignment.JUSTIFIED,
MTextParagraphAlignment.DEFAULT: tl.ParagraphAlignment.LEFT,
}
ATTACHMENT_POINT_TO_ALIGN = {
const.MTEXT_TOP_LEFT: tl.ParagraphAlignment.LEFT,
const.MTEXT_MIDDLE_LEFT: tl.ParagraphAlignment.LEFT,
const.MTEXT_BOTTOM_LEFT: tl.ParagraphAlignment.LEFT,
const.MTEXT_TOP_CENTER: tl.ParagraphAlignment.CENTER,
const.MTEXT_MIDDLE_CENTER: tl.ParagraphAlignment.CENTER,
const.MTEXT_BOTTOM_CENTER: tl.ParagraphAlignment.CENTER,
const.MTEXT_TOP_RIGHT: tl.ParagraphAlignment.RIGHT,
const.MTEXT_MIDDLE_RIGHT: tl.ParagraphAlignment.RIGHT,
const.MTEXT_BOTTOM_RIGHT: tl.ParagraphAlignment.RIGHT,
}
STACKING = {
"^": tl.Stacking.OVER,
"/": tl.Stacking.LINE,
"#": tl.Stacking.SLANTED,
}
def make_default_tab_stops(cap_height: float, width: float) -> list[tl.TabStop]:
tab_stops = []
step = 4.0 * cap_height
pos = step
while pos < width:
tab_stops.append(tl.TabStop(pos, tl.TabStopType.LEFT))
pos += step
return tab_stops
def append_default_tab_stops(
tab_stops: list[tl.TabStop], default_stops: Sequence[tl.TabStop]
) -> None:
last_pos = 0.0
if tab_stops:
last_pos = tab_stops[-1].pos
tab_stops.extend(stop for stop in default_stops if stop.pos > last_pos)
def make_tab_stops(
cap_height: float,
width: float,
tab_stops: Sequence,
default_stops: Sequence[tl.TabStop],
) -> list[tl.TabStop]:
_tab_stops = []
for stop in tab_stops:
if isinstance(stop, str):
value = float(stop[1:])
if stop[0] == "c":
kind = tl.TabStopType.CENTER
else:
kind = tl.TabStopType.RIGHT
else:
kind = tl.TabStopType.LEFT
value = float(stop)
pos = value * cap_height
if pos < width:
_tab_stops.append(tl.TabStop(pos, kind))
append_default_tab_stops(_tab_stops, default_stops)
return _tab_stops
def get_stroke(ctx: MTextContext) -> int:
stroke = 0
if ctx.underline:
stroke += tl.Stroke.UNDERLINE
if ctx.strike_through:
stroke += tl.Stroke.STRIKE_THROUGH
if ctx.overline:
stroke += tl.Stroke.OVERLINE
if ctx.continue_stroke:
stroke += tl.Stroke.CONTINUE
return stroke
def new_paragraph(
cells: list,
ctx: MTextContext,
cap_height: float,
line_spacing: float = 1,
width: float = 0,
default_stops: Optional[Sequence[tl.TabStop]] = None,
):
if cells:
p = ctx.paragraph
align = ALIGN.get(p.align, tl.ParagraphAlignment.LEFT)
left = p.left * cap_height
right = p.right * cap_height
first = left + p.indent * cap_height # relative to left
_default_stops: Sequence[tl.TabStop] = default_stops or []
tab_stops = _default_stops
if p.tab_stops:
tab_stops = make_tab_stops(cap_height, width, p.tab_stops, _default_stops)
paragraph = tl.Paragraph(
align=align,
indent=(first, left, right),
line_spacing=line_spacing,
tab_stops=tab_stops,
)
paragraph.append_content(cells)
else:
paragraph = tl.EmptyParagraph( # type: ignore
cap_height=ctx.cap_height, line_spacing=line_spacing
)
return paragraph
def super_glue():
return tl.NonBreakingSpace(width=0, min_width=0, max_width=0)
def defined_width(mtext: MText) -> float:
width = mtext.dxf.get("width", 0.0)
if width < 1e-6:
width, height = estimate_mtext_extents(mtext)
return width
def column_heights(columns: MTextColumns) -> list[Optional[float]]:
heights: list[Optional[float]]
if columns.heights: # dynamic manual
heights = list(columns.heights)
# last height has to be auto height = None
heights[-1] = None
return heights
# static, dynamic auto
defined_height = abs(columns.defined_height)
if defined_height < 1e-6:
return [None]
return [defined_height] * columns.count
class AbstractMTextRenderer(abc.ABC):
def __init__(self) -> None:
self._font_cache: dict[tuple[str, float, float], fonts.AbstractFont] = {}
@abc.abstractmethod
def word(self, test: str, ctx: MTextContext) -> tl.ContentCell:
...
@abc.abstractmethod
def fraction(self, data: tuple[str, str, str], ctx: MTextContext) -> tl.ContentCell:
...
@abc.abstractmethod
def get_font_face(self, mtext: MText) -> fonts.FontFace:
...
@abc.abstractmethod
def make_bg_renderer(self, mtext: MText) -> tl.ContentRenderer:
...
def make_mtext_context(self, mtext: MText) -> MTextContext:
ctx = MTextContext()
ctx.paragraph = ParagraphProperties(
align=ATTACHMENT_POINT_TO_ALIGN.get( # type: ignore
mtext.dxf.attachment_point, tl.ParagraphAlignment.LEFT
)
)
ctx.font_face = self.get_font_face(mtext)
ctx.cap_height = mtext.dxf.char_height
ctx.aci = mtext.dxf.color
rgb = mtext.rgb
if rgb is not None:
ctx.rgb = colors.RGB(*rgb)
return ctx
def get_font(self, ctx: MTextContext) -> fonts.AbstractFont:
ttf = fonts.find_font_file_name(ctx.font_face) # 1st call is very slow
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_stroke(self, ctx: MTextContext) -> int:
return get_stroke(ctx)
def get_stacking(self, type_: str) -> tl.Stacking:
return STACKING.get(type_, tl.Stacking.LINE)
def space_width(self, ctx: MTextContext) -> float:
return self.get_font(ctx).space_width()
def space(self, ctx: MTextContext):
return tl.Space(width=self.space_width(ctx))
def tabulator(self, ctx: MTextContext):
return tl.Tabulator(width=self.space_width(ctx))
def non_breaking_space(self, ctx: MTextContext):
return tl.NonBreakingSpace(width=self.space_width(ctx))
def layout_engine(self, mtext: MText) -> tl.Layout:
initial_cap_height = mtext.dxf.char_height
line_spacing = mtext.dxf.line_spacing_factor
def append_paragraph():
paragraph = new_paragraph(
cells,
ctx,
initial_cap_height,
line_spacing,
width,
default_stops,
)
layout.append_paragraphs([paragraph])
cells.clear()
bg_renderer = self.make_bg_renderer(mtext)
width = defined_width(mtext)
default_stops = make_default_tab_stops(initial_cap_height, width)
layout = tl.Layout(width=width)
if mtext.has_columns:
columns = mtext.columns
assert columns is not None
for height in column_heights(columns):
layout.append_column(
width=columns.width,
height=height,
gutter=columns.gutter_width,
renderer=bg_renderer,
)
else:
# column with auto height and default width
layout.append_column(renderer=bg_renderer)
content = mtext.all_columns_raw_content()
ctx = self.make_mtext_context(mtext)
cells: list[tl.Cell] = []
for token in MTextParser(content, ctx):
ctx = token.ctx
if token.type == TokenType.NEW_PARAGRAPH:
append_paragraph()
elif token.type == TokenType.NEW_COLUMN:
append_paragraph()
layout.next_column()
elif token.type == TokenType.SPACE:
cells.append(self.space(ctx))
elif token.type == TokenType.NBSP:
cells.append(self.non_breaking_space(ctx))
elif token.type == TokenType.TABULATOR:
cells.append(self.tabulator(ctx))
elif token.type == TokenType.WORD:
if cells and isinstance(cells[-1], (tl.Text, tl.Fraction)):
# Create an unbreakable connection between those two parts.
cells.append(super_glue())
cells.append(self.word(token.data, ctx))
elif token.type == TokenType.STACK:
if cells and isinstance(cells[-1], (tl.Text, tl.Fraction)):
# Create an unbreakable connection between those two parts.
cells.append(super_glue())
cells.append(self.fraction(token.data, ctx))
if cells:
append_paragraph()
return layout