680 lines
25 KiB
Python
680 lines
25 KiB
Python
#!/usr/bin/env python3
|
|
# coding=utf-8
|
|
#
|
|
# Copyright (c) 2024 jonathan.neuhauser@outlook.com
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation; either version 2 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program; if not, write to the Free Software
|
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
#
|
|
"""
|
|
Convert CGM files to SVG
|
|
"""
|
|
|
|
from dataclasses import dataclass
|
|
import inspect
|
|
from inkex.utils import pairwise
|
|
import math
|
|
from typing import Callable, Dict, List, Optional, Tuple, get_type_hints, Union
|
|
|
|
import inkex
|
|
import cgm_parse
|
|
import cgm_enums
|
|
|
|
|
|
class CGMConverterMeta(type):
|
|
"""Metaclass so that we can automatically call the methods based
|
|
on their type hint"""
|
|
|
|
def __new__(cls, name, bases, class_dict):
|
|
new_class = super().__new__(cls, name, bases, class_dict)
|
|
|
|
call_mapping = {}
|
|
for name, method in inspect.getmembers(new_class, predicate=inspect.isfunction):
|
|
hints = get_type_hints(method)
|
|
if name.startswith("on_") and len(hints) == 1:
|
|
type_ = list(hints.values())[0]
|
|
|
|
call_mapping[type_] = method
|
|
|
|
new_class.call_mapping = call_mapping
|
|
return new_class
|
|
|
|
|
|
@dataclass
|
|
class FillAttributes:
|
|
fill: cgm_enums.Colour
|
|
interior_style: cgm_enums.InteriorStyleEnum
|
|
|
|
|
|
@dataclass
|
|
class LineAttributes:
|
|
specification_mode: cgm_enums.WidthSpecificationModeEnum
|
|
width: float
|
|
colour: cgm_enums.Colour
|
|
type: cgm_enums.LineTypeEnum
|
|
visibility: cgm_enums.EdgeVisibilityEnum
|
|
|
|
|
|
@dataclass
|
|
class TextAttributes:
|
|
colour: cgm_enums.Colour
|
|
character_height: Optional[float]
|
|
fallback_character_height: Optional[float]
|
|
horizontal_alignment: cgm_enums.TextHorizontalAlignmentEnum
|
|
vertical_alignment: cgm_enums.TextVerticalAlignmentEnum
|
|
x_character_up: Optional[float]
|
|
y_character_up: Optional[float]
|
|
x_character_base: Optional[float]
|
|
y_character_base: Optional[float]
|
|
precision: cgm_enums.TextPrecisionEnum
|
|
expansion_ratio: float
|
|
character_spacing: float
|
|
path: cgm_enums.TextPathEnum
|
|
font_index: int
|
|
|
|
|
|
class CGMConverter(metaclass=CGMConverterMeta):
|
|
call_mapping: Dict[type, Callable]
|
|
|
|
def __init__(self) -> None:
|
|
self.current_page = None
|
|
self.document = inkex.InputExtension.get_template(width="0", height="0")
|
|
self.svg = self.document.getroot()
|
|
self.color_table: Dict[int, cgm_enums.DirectColour] = {}
|
|
self.vdc_height = 100
|
|
self.font_list: List[str] = ["sans-serif"]
|
|
self.default_replacement: List[cgm_parse.CGMCommand] = []
|
|
self.first_page_origin: Optional[Tuple[float, float]] = None
|
|
|
|
pass
|
|
|
|
def set_defaults(self) -> None:
|
|
self.edge_attributes = LineAttributes(
|
|
cgm_enums.WidthSpecificationModeEnum.SCALED,
|
|
1.0,
|
|
cgm_enums.DirectColour(0, 0, 0), # device-dependent fg color
|
|
cgm_enums.LineTypeEnum.SOLID,
|
|
cgm_enums.EdgeVisibilityEnum.OFF,
|
|
)
|
|
self.line_attributes = LineAttributes(
|
|
cgm_enums.WidthSpecificationModeEnum.SCALED,
|
|
1.0,
|
|
cgm_enums.DirectColour(0, 0, 0), # device-dependent fg color
|
|
cgm_enums.LineTypeEnum.SOLID,
|
|
cgm_enums.EdgeVisibilityEnum.ON,
|
|
)
|
|
self.fill_attributes = FillAttributes(
|
|
cgm_enums.DirectColour(0, 0, 0), # device-dependent fg color
|
|
interior_style=cgm_enums.InteriorStyleEnum.HOLLOW,
|
|
)
|
|
self.text_attributes = TextAttributes(
|
|
cgm_enums.DirectColour(0, 0, 0), # device-dependent fg color
|
|
None,
|
|
None,
|
|
cgm_enums.TextHorizontalAlignmentEnum.NORMAL_HORIZONTAL,
|
|
cgm_enums.TextVerticalAlignmentEnum.NORMAL_VERTICAL,
|
|
0,
|
|
None,
|
|
None,
|
|
0,
|
|
cgm_enums.TextPrecisionEnum.STRING,
|
|
1.0,
|
|
0,
|
|
cgm_enums.TextPathEnum.RIGHT,
|
|
1,
|
|
)
|
|
self.current_text: Optional[inkex.TextElement] = None
|
|
first = self.first_page_origin == None
|
|
for cmd in self.default_replacement:
|
|
self.stream(cmd)
|
|
if first:
|
|
# Restore this information if it was overwritten by the default replacement
|
|
self.first_page_origin = None
|
|
|
|
def apply_edge(self, elem):
|
|
if self.edge_attributes.visibility == cgm_enums.EdgeVisibilityEnum.ON:
|
|
elem.style.update(
|
|
{
|
|
"stroke": self.get_colour(self.edge_attributes.colour),
|
|
"stroke-width": self.edge_attributes.width,
|
|
}
|
|
)
|
|
self.apply_stroke_dasharray(elem, self.edge_attributes.type)
|
|
|
|
def apply_line(self, elem):
|
|
elem.style.update(
|
|
{
|
|
"fill": None,
|
|
"stroke": self.get_colour(self.line_attributes.colour),
|
|
"stroke-width": self.line_attributes.width,
|
|
}
|
|
)
|
|
self.apply_stroke_dasharray(elem, self.line_attributes.type)
|
|
|
|
def apply_stroke_dasharray(self, elem, stroke_type: cgm_enums.LineTypeEnum):
|
|
if stroke_type == cgm_enums.LineTypeEnum.SOLID:
|
|
return
|
|
elif stroke_type == cgm_enums.LineTypeEnum.DASH:
|
|
array = [3, 3]
|
|
elif stroke_type == cgm_enums.LineTypeEnum.DOT:
|
|
array = [1, 1]
|
|
elif stroke_type == cgm_enums.LineTypeEnum.DASH_DOT:
|
|
array = [4, 2, 1, 2]
|
|
elif stroke_type == cgm_enums.LineTypeEnum.DASH_DOT_DOT:
|
|
array = [4, 2, 1, 2, 1, 2]
|
|
else:
|
|
return # private value
|
|
elem.style["stroke-dasharray"] = [
|
|
i * float(elem.style["stroke-width"]) for i in array
|
|
]
|
|
|
|
def get_colour(self, colour: cgm_enums.Colour):
|
|
if isinstance(colour, cgm_enums.IndexColour):
|
|
colour = self.color_table.get(
|
|
colour.index,
|
|
cgm_enums.DirectColour(255, 255, 255)
|
|
if colour.index == 0
|
|
else cgm_enums.DirectColour(0, 0, 0),
|
|
)
|
|
if isinstance(colour, cgm_enums.DirectColour):
|
|
if colour.colour_model == cgm_enums.ColourModelEnum.RGB:
|
|
return inkex.Color([colour.v1, colour.v2, colour.v3])
|
|
else:
|
|
inkex.errormsg(f"Color model {colour.colour_model} not implemented")
|
|
|
|
def apply_fill(self, elem):
|
|
fill = None
|
|
if self.fill_attributes.interior_style == cgm_enums.InteriorStyleEnum.HOLLOW:
|
|
fill = None
|
|
elif self.fill_attributes.interior_style == cgm_enums.InteriorStyleEnum.SOLID:
|
|
fill = self.get_colour(self.fill_attributes.fill)
|
|
elem.style.update({"fill-rule": "evenodd", "fill": fill})
|
|
|
|
def on_begin_picture(self, arg: cgm_parse.BeginPicture):
|
|
self.current_page = self.svg.namedview.add(inkex.Page())
|
|
assert self.current_page is not None
|
|
self.current_page.set("inkscape:label", str(arg.name))
|
|
self.current_layer: inkex.Layer = self.svg.add(inkex.Layer())
|
|
|
|
self.set_defaults()
|
|
|
|
def on_vdc_extent(self, arg: cgm_parse.VDCExtent):
|
|
first_page = False
|
|
if self.first_page_origin is None:
|
|
self.first_page_origin = arg.first_corner.x, arg.first_corner.y
|
|
first_page = True
|
|
|
|
x, y, width, height = (
|
|
arg.first_corner.x - self.first_page_origin[0],
|
|
arg.first_corner.y - self.first_page_origin[1],
|
|
abs(arg.second_corner.x - arg.first_corner.x),
|
|
abs(arg.second_corner.y - arg.first_corner.y),
|
|
)
|
|
assert self.current_page is not None
|
|
|
|
self.current_page.set("x", x)
|
|
self.current_page.set("y", y)
|
|
self.current_page.set("width", width)
|
|
self.current_page.set("height", height)
|
|
self.svg.set("width", width)
|
|
self.svg.set("height", height)
|
|
# x1 = x * matrix.a + y * matrix.b + matrix.e,
|
|
# y1 = x * matrix.c + y * matrix.d + matrix.f;
|
|
|
|
# We need a transform so that VDC Extent p1 is the bottom left of the page
|
|
# and VDC Extent p2 is the bottom right of the page
|
|
|
|
a = (width) / (arg.second_corner.x - arg.first_corner.x)
|
|
d = (height) / (arg.first_corner.y - arg.second_corner.y)
|
|
e = x - arg.first_corner.x * a
|
|
f = y - arg.second_corner.y * d
|
|
|
|
trans = inkex.Transform((a, 0, 0, d, e, f))
|
|
assert (
|
|
trans.apply_to_point(arg.first_corner.x + arg.first_corner.y * 1j)
|
|
== x + (y + height) * 1j
|
|
)
|
|
assert (
|
|
trans.apply_to_point(arg.second_corner.x + arg.second_corner.y * 1j)
|
|
== (x + width) + (y) * 1j
|
|
)
|
|
|
|
self.current_layer.transform = trans
|
|
if first_page:
|
|
# only for first page
|
|
self.svg.set("viewBox", f"0 0 {width} {height}")
|
|
self.vdc_height = height
|
|
pass
|
|
|
|
def on_rectangle(self, arg: cgm_parse.Rectangle):
|
|
rect = self.current_layer.add(
|
|
inkex.Rectangle.new(
|
|
arg.p1.x, arg.p1.y, arg.p2.x - arg.p1.x, arg.p2.y - arg.p1.y
|
|
)
|
|
)
|
|
self.apply_fill(rect)
|
|
self.apply_edge(rect)
|
|
|
|
def on_circle(self, arg: cgm_parse.Circle):
|
|
circle = self.current_layer.add(
|
|
inkex.Circle.new((arg.center.x, arg.center.y), arg.radius)
|
|
)
|
|
self.apply_fill(circle)
|
|
self.apply_edge(circle)
|
|
|
|
def on_ellipse(self, arg: cgm_parse.Ellipse):
|
|
ellipse = inkex.Ellipse.new((0, 0), (1, 1))
|
|
|
|
ellipse.transform = inkex.Transform(
|
|
(
|
|
arg.point_1.x - arg.center.x,
|
|
arg.point_1.y - arg.center.y,
|
|
arg.point_2.x - arg.center.x,
|
|
arg.point_2.y - arg.center.y,
|
|
arg.center.x,
|
|
arg.center.y,
|
|
)
|
|
)
|
|
assert (
|
|
ellipse.transform.apply_to_point(1) == arg.point_1.x + (arg.point_1.y) * 1j
|
|
)
|
|
assert (
|
|
ellipse.transform.apply_to_point(1j) == arg.point_2.x + (arg.point_2.y) * 1j
|
|
)
|
|
pel: inkex.PathElement = self.current_layer.add(ellipse.to_path_element())
|
|
pel.apply_transform()
|
|
self.apply_fill(pel)
|
|
self.apply_edge(pel)
|
|
|
|
def on_polygon(self, arg: cgm_parse.Polygon):
|
|
poly = inkex.Polygon.new(" ".join([f"{i.x},{i.y}" for i in arg.points]))
|
|
|
|
path = self.current_layer.add(poly.to_path_element())
|
|
self.apply_fill(path)
|
|
self.apply_edge(path)
|
|
|
|
def on_polyline(self, arg: cgm_parse.Polyline):
|
|
poly = inkex.Polyline.new(" ".join([f"{i.x},{i.y}" for i in arg.points]))
|
|
|
|
path = self.current_layer.add(poly.to_path_element())
|
|
self.apply_line(path)
|
|
|
|
def on_disjoint_polyline(self, arg: cgm_parse.DisjointPolyline):
|
|
path = inkex.PathElement.new(
|
|
inkex.Path(
|
|
[
|
|
inkex.paths.Move(pt.x, pt.y)
|
|
if i % 2 == 0
|
|
else inkex.paths.Line(pt.x, pt.y)
|
|
for i, pt in enumerate(arg.points)
|
|
]
|
|
)
|
|
)
|
|
self.current_layer.add(path)
|
|
self.apply_line(path)
|
|
|
|
def on_polygon_set(self, arg: cgm_parse.PolygonSet):
|
|
current_close_point = cgm_enums.Point(0, 0)
|
|
if len(arg.points) == 0:
|
|
return
|
|
|
|
polygons: List[List[Tuple[bool, cgm_enums.Point]]] = []
|
|
|
|
# First construct a list of polygons in the set.
|
|
def iterator(pts):
|
|
yield from pairwise(pts)
|
|
yield [pts[-1], None]
|
|
|
|
for e1, e2 in iterator(arg.points):
|
|
if e1 is None:
|
|
polygons.append([])
|
|
current_close_point = e2.pt
|
|
else:
|
|
if e2 is not None and e1.linetype in (
|
|
cgm_enums.PolygonSetLineTypeEnum.INVISIBLE,
|
|
cgm_enums.PolygonSetLineTypeEnum.VISIBLE,
|
|
):
|
|
polygons[-1].append(
|
|
(e1.linetype == cgm_enums.PolygonSetLineTypeEnum.VISIBLE, e2.pt)
|
|
)
|
|
else:
|
|
draw = e1.linetype in (
|
|
cgm_enums.PolygonSetLineTypeEnum.CLOSE_VISIBLE,
|
|
cgm_enums.PolygonSetLineTypeEnum.VISIBLE,
|
|
)
|
|
polygons[-1].append((draw, current_close_point))
|
|
if e2 is not None:
|
|
current_close_point = e2.pt
|
|
polygons.append([])
|
|
|
|
# Now re-shuffle each polygon so we start with an invisible
|
|
# line if it exists. Coordinates are absolute so this should be fine
|
|
shuffled = []
|
|
single_element = True
|
|
for poly in polygons:
|
|
has_invisible = any([not j[0] for j in poly])
|
|
if has_invisible:
|
|
inv_index = ([p[0] for p in poly]).index(False)
|
|
shuffled.append(poly[inv_index:] + poly[:inv_index])
|
|
else:
|
|
shuffled.append(poly)
|
|
if has_invisible:
|
|
single_element = False
|
|
|
|
# Draw the fill, then the stroke.
|
|
fill_path = inkex.Path()
|
|
for poly in shuffled:
|
|
fill_path.append(inkex.paths.Move(poly[-1][1].x, poly[-1][1].y))
|
|
for point in poly[:-1]: # The last line is drawn via ZoneClose
|
|
fill_path.append(inkex.paths.Line(point[1].x, point[1].y))
|
|
fill_path.append(inkex.paths.ZoneClose())
|
|
fill_elem = self.current_layer.add(inkex.PathElement.new(fill_path))
|
|
self.apply_fill(fill_elem)
|
|
|
|
if single_element:
|
|
self.apply_edge(fill_elem)
|
|
|
|
else:
|
|
# Draw the stroke separately
|
|
stroke_path = inkex.Path()
|
|
|
|
for poly in shuffled:
|
|
stroke_path.append(inkex.paths.Move(poly[-1][1].x, poly[-1][1].y))
|
|
for point in poly:
|
|
if point[0]:
|
|
stroke_path.append(inkex.paths.Line(point[1].x, point[1].y))
|
|
else:
|
|
stroke_path.append(inkex.paths.Move(point[1].x, point[1].y))
|
|
if all(i[0] for i in poly):
|
|
stroke_path.append(inkex.paths.ZoneClose())
|
|
stroke_elem = self.current_layer.add(inkex.PathElement.new(stroke_path))
|
|
self.apply_edge(stroke_elem)
|
|
stroke_elem.style["fill"] = None
|
|
|
|
def on_polybezier(self, arg: cgm_parse.Polybezier):
|
|
if len(arg.points) == 0:
|
|
return
|
|
|
|
path = inkex.Path()
|
|
if arg.continuous == cgm_enums.PolybezierContinuityEnum.CONTINUOUS:
|
|
assert len(arg.points) % 3 == 1, "Bad Polybezier specification"
|
|
path.append(inkex.paths.Move(complex(arg.points[0])))
|
|
for i in range((len(arg.points) - 1) // 3):
|
|
path.append(
|
|
inkex.paths.Curve(
|
|
complex(arg.points[3 * i + 1]),
|
|
complex(arg.points[3 * i + 2]),
|
|
complex(arg.points[3 * i + 3]),
|
|
)
|
|
)
|
|
else:
|
|
assert len(arg.points) % 4 == 0, "Bad Polybezier specification"
|
|
for i in range(len(arg.points) // 4):
|
|
if i == 0 or path[-1].cend_point(0, 0) != complex(arg.points[4 * i]):
|
|
path.append(inkex.paths.Move(complex(arg.points[4 * i])))
|
|
path.append(
|
|
inkex.paths.Curve(
|
|
complex(arg.points[4 * i + 1]),
|
|
complex(arg.points[4 * i + 2]),
|
|
complex(arg.points[4 * i + 3]),
|
|
)
|
|
)
|
|
pel = self.current_layer.add(inkex.PathElement.new(path))
|
|
self.apply_line(pel)
|
|
pel.style["stroke"] = "red"
|
|
|
|
def on_circular_arc_3pt(self, arg: cgm_parse.CircularArc3Point):
|
|
path = inkex.PathElement.arc_from_3_points(
|
|
complex(arg.p1), complex(arg.p2), complex(arg.p3), "arc"
|
|
)
|
|
self.current_layer.append(path)
|
|
self.apply_line(path)
|
|
|
|
def on_circular_arc_3pt_close(self, arg: cgm_parse.CircularArc3PointClose):
|
|
path = inkex.PathElement.arc_from_3_points(
|
|
complex(arg.p1),
|
|
complex(arg.p2),
|
|
complex(arg.p3),
|
|
"chord" if arg.closure == cgm_enums.ArcClosureEnum.CHORD else "slice",
|
|
)
|
|
self.current_layer.append(path)
|
|
self.apply_fill(path)
|
|
self.apply_edge(path)
|
|
|
|
def circular_arc_helper(
|
|
self, arg: Union[cgm_parse.CircularArcCentre, cgm_parse.CircularArcCentreClose]
|
|
):
|
|
c = complex(arg.centre)
|
|
start_ray = arg.delta_x_start + arg.delta_y_start * 1j
|
|
end_ray = arg.delta_x_end + arg.delta_y_end * 1j
|
|
start_angle = inkex.Vector2d(start_ray).angle
|
|
end_angle = inkex.Vector2d(end_ray).angle
|
|
arctype = "arc"
|
|
if isinstance(arg, cgm_parse.CircularArcCentreClose):
|
|
arctype = (
|
|
"chord" if arg.closure == cgm_enums.ArcClosureEnum.CHORD else "slice"
|
|
)
|
|
path = inkex.PathElement.arc(
|
|
inkex.Vector2d(c),
|
|
arg.radius,
|
|
arg.radius,
|
|
arctype,
|
|
start=start_angle,
|
|
end=end_angle,
|
|
)
|
|
return path
|
|
|
|
def on_circular_arc_centre(self, arg: cgm_parse.CircularArcCentre):
|
|
path = self.circular_arc_helper(arg)
|
|
self.current_layer.append(path)
|
|
self.apply_line(path)
|
|
|
|
def on_circular_arc_centre_close(self, arg: cgm_parse.CircularArcCentreClose):
|
|
path = self.circular_arc_helper(arg)
|
|
self.current_layer.append(path)
|
|
self.apply_fill(path)
|
|
self.apply_edge(path)
|
|
|
|
def on_interior_style(self, arg: cgm_parse.InteriorStyle):
|
|
self.fill_attributes.interior_style = arg.interior_style
|
|
|
|
def on_fill_colour(self, arg: cgm_parse.FillColour):
|
|
self.fill_attributes.fill = arg.fill
|
|
|
|
def on_color_table(self, arg: cgm_parse.ColourTable):
|
|
for i, col in enumerate(arg.colours):
|
|
self.color_table[i + arg.colour_index.index] = col
|
|
|
|
def on_line_width(self, arg: cgm_parse.LineWidth):
|
|
if arg.width > 0:
|
|
self.line_attributes.width = arg.width
|
|
|
|
def on_line_width_specification_mode(
|
|
self, arg: cgm_parse.LineWidthSpecificationMode
|
|
):
|
|
self.line_attributes.specification_mode = arg.size_specification
|
|
|
|
def on_line_colour(self, arg: cgm_parse.LineColour):
|
|
self.line_attributes.colour = arg.colour
|
|
|
|
def on_line_type(self, arg: cgm_parse.LineType):
|
|
self.line_attributes.type = arg.type
|
|
|
|
def on_edge_width(self, arg: cgm_parse.EdgeWidth):
|
|
if arg.width > 0:
|
|
self.edge_attributes.width = arg.width
|
|
|
|
def on_edge_colour(self, arg: cgm_parse.EdgeColour):
|
|
self.edge_attributes.colour = arg.colour
|
|
|
|
def on_edge_type(self, arg: cgm_parse.EdgeType):
|
|
self.edge_attributes.type = arg.type
|
|
|
|
def on_edge_visibility(self, arg: cgm_parse.EdgeVisibility):
|
|
self.edge_attributes.visibility = arg.visibility
|
|
|
|
def on_background(self, arg: cgm_parse.BackgroundColour):
|
|
self.svg.namedview.set("pagecolor", str(self.get_colour(arg.colour)))
|
|
|
|
# Text-related
|
|
|
|
def on_font_list(self, arg: cgm_parse.FontList):
|
|
self.font_list = arg.fonts
|
|
|
|
def on_text_font_index(self, arg: cgm_parse.TextFontIndex):
|
|
self.text_attributes.font_index = arg.index
|
|
|
|
def on_text_alignment(self, arg: cgm_parse.TextAlignment):
|
|
self.text_attributes.horizontal_alignment = arg.horizontal_alignment
|
|
self.text_attributes.vertical_alignment = arg.vertical_alignment
|
|
|
|
def on_character_height(self, arg: cgm_parse.CharacterHeight):
|
|
self.text_attributes.character_height = arg.height
|
|
|
|
def on_character_orientation(self, arg: cgm_parse.CharacterOrientation):
|
|
self.text_attributes.x_character_base = arg.x_character_base
|
|
self.text_attributes.x_character_up = arg.x_character_up
|
|
self.text_attributes.y_character_base = arg.y_character_base
|
|
self.text_attributes.y_character_up = arg.y_character_up
|
|
self.text_attributes.fallback_character_height = math.sqrt(
|
|
arg.x_character_up**2 + arg.y_character_up**2
|
|
)
|
|
|
|
def on_text_precision(self, arg: cgm_parse.TextPrecision):
|
|
self.text_attributes.precision = arg.text_precision
|
|
|
|
def on_character_spacing(self, arg: cgm_parse.CharacterSpacing):
|
|
self.text_attributes.character_spacing = arg.spacing
|
|
|
|
def on_text_path(self, arg: cgm_parse.TextPath):
|
|
self.text_attributes.path = arg.path
|
|
|
|
def on_character_expansion(self, arg: cgm_parse.CharacterExpansionFactor):
|
|
self.text_attributes.expansion_ratio = arg.factor
|
|
|
|
def on_text_colour(self, arg: cgm_parse.TextColour):
|
|
self.text_attributes.colour = arg.colour
|
|
|
|
def add_tspan(self, text: str):
|
|
assert self.current_text is not None
|
|
tspan = self.current_text.add(inkex.Tspan())
|
|
tspan.text = text
|
|
ch = (
|
|
self.text_attributes.character_height
|
|
or self.text_attributes.fallback_character_height
|
|
)
|
|
tspan.style.update(
|
|
{
|
|
"font-size": 1 / 100 * self.vdc_height if ch is None else ch,
|
|
"font-stretch": f"{self.text_attributes.expansion_ratio * 100}%",
|
|
"letter-spacing": self.text_attributes.character_spacing,
|
|
"fill": self.get_colour(self.text_attributes.colour),
|
|
# 1-based indices
|
|
"font-family": self.font_list[self.text_attributes.font_index - 1],
|
|
}
|
|
)
|
|
|
|
def on_text(self, arg: cgm_parse.Text):
|
|
self.current_text = inkex.TextElement()
|
|
# Apply transform
|
|
# get baseline unit vector
|
|
cuv = inkex.Vector2d(
|
|
0
|
|
if self.text_attributes.x_character_up is None
|
|
else self.text_attributes.x_character_up,
|
|
self.vdc_height
|
|
if self.text_attributes.y_character_up is None
|
|
else self.text_attributes.y_character_up,
|
|
)
|
|
cbv = inkex.Vector2d(
|
|
self.vdc_height
|
|
if self.text_attributes.x_character_base is None
|
|
else self.text_attributes.x_character_base,
|
|
0
|
|
if self.text_attributes.y_character_base is None
|
|
else self.text_attributes.y_character_base,
|
|
)
|
|
cuv = 1 / cuv.length * cuv
|
|
cbv = 1 / cbv.length * cbv
|
|
|
|
self.current_text.transform = inkex.Transform(
|
|
(cbv.x, cbv.y, cuv.x, cuv.y, 0, 0)
|
|
) @ inkex.Transform((1, 0, 0, -1, 0, 0))
|
|
origin = arg.point.x + arg.point.y * 1j
|
|
# Apply reverse transform to origin
|
|
origin = (-self.current_text.transform).apply_to_point(origin)
|
|
self.current_text.set("x", origin.x)
|
|
self.current_text.set("y", origin.y)
|
|
|
|
if (
|
|
self.text_attributes.horizontal_alignment
|
|
== cgm_enums.TextHorizontalAlignmentEnum.CENTER
|
|
):
|
|
self.current_text.style["text-anchor"] = "middle"
|
|
elif (
|
|
self.text_attributes.horizontal_alignment
|
|
== cgm_enums.TextHorizontalAlignmentEnum.RIGHT
|
|
):
|
|
self.current_text.style["text-anchor"] = "end"
|
|
|
|
if (
|
|
self.text_attributes.vertical_alignment
|
|
== cgm_enums.TextVerticalAlignmentEnum.HALF
|
|
):
|
|
self.current_text.set("dy", "0.4em")
|
|
if (
|
|
self.text_attributes.vertical_alignment
|
|
== cgm_enums.TextVerticalAlignmentEnum.TOP
|
|
):
|
|
self.current_text.set("dy", "1em")
|
|
|
|
# TODO: character direction, vertical alignment
|
|
|
|
self.add_tspan(arg.string)
|
|
if arg.flag == cgm_enums.TextFinalFlag.FINAL:
|
|
self.finalize_text()
|
|
|
|
def finalize_text(self):
|
|
max_font_size = max([i.style["font-size"] for i in self.current_text])
|
|
# This is for vertical alignment, which is specified in em of the largest
|
|
# text of the box
|
|
self.current_text.style["font-size"] = max_font_size
|
|
|
|
self.current_layer.append(self.current_text)
|
|
|
|
def on_default_replacement(self, arg: cgm_parse.MetafileDefaultsReplacement):
|
|
self.default_replacement = arg.data
|
|
|
|
def stream(self, arg: cgm_parse.CGMCommand):
|
|
handler = self.call_mapping.get(type(arg))
|
|
if handler: # otherwise we ignore the command
|
|
handler(self, arg)
|
|
|
|
|
|
class CgmInput(inkex.InputExtension):
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
|
|
self.converter = CGMConverter()
|
|
|
|
def load(self, stream):
|
|
self.parser = cgm_parse.BinaryCGMCommandParser(stream)
|
|
for command in self.parser.parse():
|
|
self.converter.stream(command)
|
|
|
|
return self.converter.document
|
|
|
|
|
|
if __name__ == "__main__":
|
|
CgmInput().run()
|