297 lines
9.6 KiB
Python
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
|