Files
aza/APP/logo-tool/core/cairo_renderer.py

102 lines
3.0 KiB
Python
Raw Permalink Normal View History

2026-03-30 07:59:11 +02:00
"""
Cairo-basiertes Rendering der Vektorpfade.
Erzeugt aus den manipulierten Pfaden ein neues Bild (für Preview und Export).
"""
from __future__ import annotations
import cairo
import numpy as np
from PySide6.QtGui import QImage, QPixmap
from svgpathtools import Path as SvgPath, Line, CubicBezier, QuadraticBezier
from core.logo_model import LogoProject, GlyphGroup
def _walk_svg_path(ctx: cairo.Context, svg_path: SvgPath):
"""Zeichnet einen svgpathtools-Pfad auf einen Cairo-Context."""
for seg in svg_path:
start = seg.start
if isinstance(seg, Line):
ctx.line_to(seg.end.real, seg.end.imag)
elif isinstance(seg, CubicBezier):
ctx.curve_to(
seg.control1.real, seg.control1.imag,
seg.control2.real, seg.control2.imag,
seg.end.real, seg.end.imag,
)
elif isinstance(seg, QuadraticBezier):
# Quadratische Bezier → kubische Approximation
p0 = seg.start
p1 = seg.control
p2 = seg.end
cp1 = p0 + (2 / 3) * (p1 - p0)
cp2 = p2 + (2 / 3) * (p1 - p2)
ctx.curve_to(
cp1.real, cp1.imag,
cp2.real, cp2.imag,
p2.real, p2.imag,
)
def render_project(project: LogoProject) -> np.ndarray:
"""
Rendert das gesamte Projekt mit Cairo und gibt RGBA-Pixels zurück.
Berücksichtigt globale Strichdicke und Buchstabenabstand.
"""
w = max(project.width, 1)
h = max(project.height, 1)
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, w, h)
ctx = cairo.Context(surface)
ctx.set_source_rgba(0, 0, 0, 0)
ctx.paint()
ctx.set_source_rgba(0, 0, 0, 1)
cumulative_offset = 0.0
for glyph in project.glyphs:
ctx.save()
spacing = project.global_letter_spacing + glyph.offset_x
ctx.translate(cumulative_offset + spacing, glyph.offset_y)
cumulative_offset += spacing
stroke_w = project.global_stroke_width * glyph.stroke_width_factor
ctx.set_line_width(stroke_w)
for svg_path in glyph.paths:
if not svg_path:
continue
first_pt = svg_path[0].start
ctx.move_to(first_pt.real, first_pt.imag)
_walk_svg_path(ctx, svg_path)
ctx.close_path()
ctx.fill_preserve()
if stroke_w > 0:
ctx.stroke()
else:
ctx.new_path()
ctx.restore()
buf = surface.get_data()
arr = np.frombuffer(buf, dtype=np.uint8).reshape((h, w, 4)).copy()
# Cairo liefert BGRA, wir brauchen RGBA
arr[:, :, [0, 2]] = arr[:, :, [2, 0]]
return arr
def render_to_qpixmap(project: LogoProject) -> QPixmap:
"""Rendert das Projekt und gibt ein QPixmap zurück."""
pixels = render_project(project)
h, w, _ = pixels.shape
qimg = QImage(
pixels.data.tobytes(),
w, h,
4 * w,
QImage.Format.Format_RGBA8888,
)
return QPixmap.fromImage(qimg)