mirror of
https://github.com/enzet/map-machine.git
synced 2025-05-25 15:06:32 +02:00
Rename Röntgen to Map Machine.
This commit is contained in:
parent
38c4e00de3
commit
2bd89a6539
57 changed files with 252 additions and 236 deletions
306
map_machine/drawing.py
Normal file
306
map_machine/drawing.py
Normal file
|
@ -0,0 +1,306 @@
|
|||
"""
|
||||
Drawing utility.
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
import cairo
|
||||
import numpy as np
|
||||
import svgwrite
|
||||
from cairo import Context, ImageSurface
|
||||
from colour import Color
|
||||
from svgwrite.base import BaseElement
|
||||
from svgwrite.path import Path as SVGPath
|
||||
from svgwrite.shapes import Rect
|
||||
from svgwrite.text import Text
|
||||
|
||||
__author__ = "Sergey Vartanov"
|
||||
__email__ = "me@enzet.ru"
|
||||
|
||||
PathCommands = list[Union[float, str, np.ndarray]]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Style:
|
||||
"""
|
||||
Drawing element style.
|
||||
"""
|
||||
|
||||
fill: Optional[Color] = None
|
||||
stroke: Optional[Color] = None
|
||||
width: float = 1
|
||||
|
||||
def update_svg_element(self, element: BaseElement) -> None:
|
||||
"""Set style for SVG element."""
|
||||
if self.fill is not None:
|
||||
element.update({"fill": self.fill})
|
||||
else:
|
||||
element.update({"fill": "none"})
|
||||
if self.stroke is not None:
|
||||
element.update({"stroke": self.stroke, "stroke-width": self.width})
|
||||
|
||||
def draw_png_fill(self, context: Context) -> None:
|
||||
"""Set style for context and draw fill."""
|
||||
context.set_source_rgba(
|
||||
self.fill.get_red(), self.fill.get_green(), self.fill.get_blue(), 1
|
||||
)
|
||||
context.fill()
|
||||
|
||||
def draw_png_stroke(self, context: Context) -> None:
|
||||
"""Set style for context and draw stroke."""
|
||||
context.set_source_rgba(
|
||||
self.stroke.get_red(),
|
||||
self.stroke.get_green(),
|
||||
self.stroke.get_blue(),
|
||||
1,
|
||||
)
|
||||
context.set_line_width(self.width)
|
||||
context.stroke()
|
||||
|
||||
|
||||
class Drawing:
|
||||
"""
|
||||
Image.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: Path, width: int, height: int) -> None:
|
||||
self.file_path: Path = file_path
|
||||
self.width: int = width
|
||||
self.height: int = height
|
||||
|
||||
def rectangle(
|
||||
self, point_1: np.ndarray, point_2: np.ndarray, style: Style
|
||||
) -> None:
|
||||
"""Draw rectangle."""
|
||||
raise NotImplementedError
|
||||
|
||||
def line(self, points: list[np.ndarray], style: Style) -> None:
|
||||
"""Draw line."""
|
||||
raise NotImplementedError
|
||||
|
||||
def path(self, commands: PathCommands, style: Style) -> None:
|
||||
"""Draw path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def text(
|
||||
self, text: str, point: np.ndarray, color: Color = Color("black")
|
||||
) -> None:
|
||||
"""Draw text."""
|
||||
raise NotImplementedError
|
||||
|
||||
def write(self) -> None:
|
||||
"""Write image to the file."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SVGDrawing(Drawing):
|
||||
"""
|
||||
SVG image.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: Path, width: int, height: int) -> None:
|
||||
super().__init__(file_path, width, height)
|
||||
self.image: svgwrite.Drawing = svgwrite.Drawing(
|
||||
str(file_path), (width, height)
|
||||
)
|
||||
|
||||
def rectangle(
|
||||
self, point_1: np.ndarray, point_2: np.ndarray, style: Style
|
||||
) -> None:
|
||||
"""Draw rectangle."""
|
||||
size: np.ndarray = point_2 - point_1
|
||||
rectangle: Rect = Rect(
|
||||
(float(point_1[0]), float(point_1[1])),
|
||||
(float(size[0]), float(size[1])),
|
||||
)
|
||||
style.update_svg_element(rectangle)
|
||||
self.image.add(rectangle)
|
||||
|
||||
def line(self, points: list[np.ndarray], style: Style) -> None:
|
||||
"""Draw line."""
|
||||
commands: PathCommands = ["M"]
|
||||
for point in points:
|
||||
commands += [point, "L"]
|
||||
self.path(commands[:-1], style)
|
||||
|
||||
def path(self, commands: PathCommands, style: Style) -> None:
|
||||
"""Draw path."""
|
||||
path: SVGPath = SVGPath(d=commands)
|
||||
style.update_svg_element(path)
|
||||
self.image.add(path)
|
||||
|
||||
def text(
|
||||
self, text: str, point: np.ndarray, color: Color = Color("black")
|
||||
) -> None:
|
||||
"""Draw text."""
|
||||
self.image.add(
|
||||
Text(text, (float(point[0]), float(point[1])), fill=color)
|
||||
)
|
||||
|
||||
def write(self) -> None:
|
||||
"""Write image to the SVG file."""
|
||||
with self.file_path.open("w+") as output_file:
|
||||
self.image.write(output_file)
|
||||
|
||||
|
||||
class PNGDrawing(Drawing):
|
||||
"""
|
||||
PNG image.
|
||||
"""
|
||||
|
||||
def __init__(self, file_path: Path, width: int, height: int) -> None:
|
||||
super().__init__(file_path, width, height)
|
||||
self.surface: ImageSurface = ImageSurface(
|
||||
cairo.FORMAT_ARGB32, width, height
|
||||
)
|
||||
self.context: Context = Context(self.surface)
|
||||
|
||||
def rectangle(
|
||||
self, point_1: np.ndarray, point_2: np.ndarray, style: Style
|
||||
) -> None:
|
||||
"""Draw rectangle."""
|
||||
size: np.ndarray = point_2 - point_1
|
||||
|
||||
if style.fill is not None:
|
||||
self.context.rectangle(point_1[0], point_1[1], size[0], size[1])
|
||||
style.draw_png_fill(self.context)
|
||||
if style.stroke is not None:
|
||||
self.context.rectangle(point_1[0], point_1[1], size[0], size[1])
|
||||
style.draw_png_stroke(self.context)
|
||||
|
||||
def line(self, points: list[np.ndarray], style: Style) -> None:
|
||||
"""Draw line."""
|
||||
if style.fill is not None:
|
||||
self.context.move_to(float(points[0][0]), float(points[0][1]))
|
||||
for point in points[1:]:
|
||||
self.context.line_to(float(point[0]), float(point[1]))
|
||||
style.draw_png_fill(self.context)
|
||||
if style.stroke is not None:
|
||||
self.context.move_to(float(points[0][0]), float(points[0][1]))
|
||||
for point in points[1:]:
|
||||
self.context.line_to(float(point[0]), float(point[1]))
|
||||
style.draw_png_stroke(self.context)
|
||||
|
||||
def _do_path(self, commands: PathCommands) -> None:
|
||||
"""Draw path."""
|
||||
current: np.ndarray = np.array((0, 0))
|
||||
start_point: Optional[np.ndarray] = None
|
||||
command: str = "M"
|
||||
is_absolute: bool = True
|
||||
|
||||
index: int = 0
|
||||
while index < len(commands):
|
||||
element: Union[float, str, np.ndarray] = commands[index]
|
||||
|
||||
if isinstance(element, str):
|
||||
is_absolute: bool = element.lower() != element
|
||||
command: str = element.lower()
|
||||
if command == "z":
|
||||
self.context.line_to(start_point[0], start_point[1])
|
||||
current = start_point
|
||||
start_point = None
|
||||
|
||||
elif command in "ml":
|
||||
point: np.ndarray
|
||||
if is_absolute:
|
||||
point = commands[index]
|
||||
else:
|
||||
point = current + commands[index]
|
||||
current = point
|
||||
if command == "m":
|
||||
self.context.move_to(point[0], point[1])
|
||||
if command == "l":
|
||||
self.context.line_to(point[0], point[1])
|
||||
if start_point is None:
|
||||
start_point = point
|
||||
|
||||
elif command == "c":
|
||||
if is_absolute:
|
||||
point_1: np.ndarray = commands[index]
|
||||
point_2: np.ndarray = commands[index + 1]
|
||||
point_3: np.ndarray = commands[index + 2]
|
||||
else:
|
||||
point_1: np.ndarray = current + commands[index]
|
||||
point_2: np.ndarray = current + commands[index + 1]
|
||||
point_3: np.ndarray = current + commands[index + 2]
|
||||
current = point_3
|
||||
self.context.curve_to(
|
||||
point_1[0], point_1[1],
|
||||
point_2[0], point_2[1],
|
||||
point_3[0], point_3[1],
|
||||
) # fmt: skip
|
||||
if start_point is None:
|
||||
start_point = point_1
|
||||
index += 2
|
||||
|
||||
elif command in "vh":
|
||||
assert isinstance(commands[index], float)
|
||||
point: np.ndarray
|
||||
if is_absolute:
|
||||
if command == "v":
|
||||
point = np.array((0, commands[index]))
|
||||
else:
|
||||
point = np.array((commands[index], 0))
|
||||
else:
|
||||
if command == "v":
|
||||
point = current + np.array((0, commands[index]))
|
||||
else:
|
||||
point = current + np.array((commands[index], 0))
|
||||
current = point
|
||||
self.context.line_to(point[0], point[1])
|
||||
if start_point is None:
|
||||
start_point = point
|
||||
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
index += 1
|
||||
|
||||
def path(self, commands: PathCommands, style: Style) -> None:
|
||||
"""Draw path."""
|
||||
if style.fill is not None:
|
||||
self._do_path(commands)
|
||||
style.draw_png_fill(self.context)
|
||||
if style.stroke is not None:
|
||||
self._do_path(commands)
|
||||
style.draw_png_stroke(self.context)
|
||||
|
||||
def text(
|
||||
self, text: str, point: np.ndarray, color: Color = Color("black")
|
||||
) -> None:
|
||||
"""Draw text."""
|
||||
self.context.set_source_rgb(
|
||||
color.get_red(), color.get_green(), color.get_blue()
|
||||
)
|
||||
self.context.move_to(float(point[0]), float(point[1]))
|
||||
self.context.show_text(text)
|
||||
|
||||
def write(self) -> None:
|
||||
"""Write image to the PNG file."""
|
||||
self.surface.write_to_png(str(self.file_path))
|
||||
|
||||
|
||||
def parse_path(path: str) -> PathCommands:
|
||||
"""Parse path command from text representation into list."""
|
||||
parts: list[str] = path.split(" ")
|
||||
result: PathCommands = []
|
||||
command: str = "M"
|
||||
index: int = 0
|
||||
while index < len(parts):
|
||||
part: str = parts[index]
|
||||
if part in "CcLlMmZzVvHh":
|
||||
result.append(part)
|
||||
command = part
|
||||
elif command in "VvHh":
|
||||
result.append(float(part))
|
||||
else:
|
||||
if "," in part:
|
||||
elements: list[str] = part.split(",")
|
||||
result.append(np.array(list(map(float, elements))))
|
||||
else:
|
||||
result.append(np.array((float(part), float(parts[index + 1]))))
|
||||
index += 1
|
||||
index += 1
|
||||
|
||||
return result
|
Loading…
Add table
Add a link
Reference in a new issue