refactor: excel parse
This commit is contained in:
@@ -0,0 +1,388 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user