fixed text formatting
newlines broke when editing characters next to newlines due to different line handling in krita and inkscape
This commit is contained in:
@@ -196,6 +196,155 @@ def prefix_ids(root, prefix):
|
||||
el.text = _URL_RE.sub(sub, el.text)
|
||||
|
||||
|
||||
XML_SPACE = "{http://www.w3.org/XML/1998/namespace}space"
|
||||
_NUM = r"[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?"
|
||||
|
||||
|
||||
def _single_num(v):
|
||||
"""Einzelwert einer x/y/dy-Angabe; None bei Listen (Einzelzeichen-Positionierung)."""
|
||||
if v is None:
|
||||
return None
|
||||
parts = [p for p in re.split(r"[\s,]+", v.strip()) if p]
|
||||
if len(parts) != 1:
|
||||
return None
|
||||
try:
|
||||
return float(parts[0])
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _style_get(style, key):
|
||||
m = re.search(r"(?:^|;)\s*%s\s*:\s*([^;]+)" % re.escape(key), style or "")
|
||||
return m.group(1).strip() if m else None
|
||||
|
||||
|
||||
def _style_set(style, key, value):
|
||||
parts = [p for p in (style or "").split(";") if p.strip()
|
||||
and p.split(":", 1)[0].strip() != key]
|
||||
parts.append("%s:%s" % (key, value))
|
||||
return ";".join(p.strip() for p in parts)
|
||||
|
||||
|
||||
def _split_blocks(steps, tol=0.02):
|
||||
"""Zeilenabstände in Blöcke mit gemeinsamem Grundabstand teilen.
|
||||
Rückgabe: Liste (erste_zeile, letzte_zeile, grundabstand, [vielfache])."""
|
||||
blocks, start, base, mults_steps = [], 0, None, []
|
||||
for i, st in enumerate(steps): # st = Abstand Zeile i -> i+1
|
||||
cand = st if base is None else min(base, st)
|
||||
trial = mults_steps + [st]
|
||||
if all(abs(x - round(x / cand) * cand) <= tol * cand for x in trial):
|
||||
base, mults_steps = cand, trial
|
||||
else:
|
||||
blocks.append((start, i, base, [int(round(x / base)) for x in mults_steps] if base else []))
|
||||
start, base, mults_steps = i + 1, None, []
|
||||
n = len(steps) + 1
|
||||
blocks.append((start, n - 1, base, [int(round(x / base)) for x in mults_steps] if base else []))
|
||||
return blocks
|
||||
|
||||
|
||||
def inkscape_text_lines(root):
|
||||
"""Krita-Mehrzeilentext (tspan mit x + relativem dy) in Inkscape-Zeilen
|
||||
(sodipodi:role="line", absolutes y, line-height) umbauen. Wechselt der
|
||||
Zeilenabstand, wird der Text in mehrere Textobjekte (Absätze) geteilt,
|
||||
weil Inkscape Abstände zwischen Zeilen mit unterschiedlichem line-height mittelt.
|
||||
Rückgabe: (umgebaut, davon geteilt, unverändert gelassen)."""
|
||||
role = q(SODI_NS, "role")
|
||||
tspan_tag = q(SVG_NS, "tspan")
|
||||
text_tag = q(SVG_NS, "text")
|
||||
parents = {c: p for p in root.iter() for c in p}
|
||||
fixed = split = kept = 0
|
||||
for text in list(root.iter(text_tag)):
|
||||
# Krita rendert führende/abschließende Leerzeichen -> in Inkscape erhalten
|
||||
text.set(XML_SPACE, "preserve")
|
||||
style = text.get("style", "")
|
||||
fs = _style_get(style, "font-size")
|
||||
if fs and re.fullmatch(_NUM, fs): # einheitenlos ist in CSS ungültig
|
||||
style = _style_set(style, "font-size", fs + "px")
|
||||
anchor = text.attrib.pop("text-anchor", None) or _style_get(style, "text-anchor")
|
||||
if anchor:
|
||||
style = _style_set(style, "text-anchor", anchor)
|
||||
align = {"middle": "center", "end": "end"}.get(anchor, "start")
|
||||
style = _style_set(style, "text-align", align)
|
||||
text.set("style", style)
|
||||
|
||||
kids = list(text)
|
||||
if not kids: # einfacher Einzeiler
|
||||
continue
|
||||
if ((text.text or "").strip()
|
||||
or any(k.tag != tspan_tag or (k.tail or "").strip() for k in kids)
|
||||
or any(k.get(role) for k in kids)):
|
||||
kept += 1
|
||||
continue
|
||||
|
||||
# absolute Grundlinien und x je Zeile bestimmen
|
||||
ys, xs, ok = [], [], True
|
||||
cur_y = _single_num(text.get("y")) or 0.0
|
||||
cur_x = _single_num(text.get("x")) or 0.0
|
||||
for k in kids:
|
||||
for attr in ("x", "y", "dx", "dy"):
|
||||
if k.get(attr) is not None and _single_num(k.get(attr)) is None:
|
||||
ok = False
|
||||
if k.get("dx") is not None:
|
||||
ok = False
|
||||
if not ok:
|
||||
break
|
||||
if k.get("x") is not None:
|
||||
cur_x = _single_num(k.get("x"))
|
||||
if k.get("y") is not None:
|
||||
cur_y = _single_num(k.get("y"))
|
||||
if k.get("dy") is not None:
|
||||
cur_y += _single_num(k.get("dy"))
|
||||
xs.append(cur_x)
|
||||
ys.append(cur_y)
|
||||
steps = [b2 - a2 for a2, b2 in zip(ys, ys[1:])]
|
||||
if not ok or any(st <= 0 for st in steps):
|
||||
kept += 1
|
||||
continue
|
||||
|
||||
blocks = _split_blocks(steps)
|
||||
for k in kids:
|
||||
text.remove(k)
|
||||
k.attrib.pop("dy", None)
|
||||
|
||||
def fill(el, first, last, base, mults):
|
||||
for i in range(first, last + 1):
|
||||
if i > first:
|
||||
for e in range(1, mults[i - first - 1]): # Leerzeilen
|
||||
empty = ET.SubElement(el, tspan_tag)
|
||||
empty.set(role, "line")
|
||||
empty.set("x", "%.6g" % xs[i])
|
||||
empty.set("y", "%.6g" % (ys[i - 1] + e * base))
|
||||
k = kids[i]
|
||||
k.set(role, "line")
|
||||
k.set("x", "%.6g" % xs[i])
|
||||
k.set("y", "%.6g" % ys[i])
|
||||
el.append(k)
|
||||
el.set("x", "%.6g" % xs[first])
|
||||
el.set("y", "%.6g" % ys[first])
|
||||
if base:
|
||||
el.set("style", _style_set(el.get("style", ""), "line-height", "%.6gpx" % base))
|
||||
|
||||
if len(blocks) == 1:
|
||||
fill(text, *blocks[0])
|
||||
fixed += 1
|
||||
continue
|
||||
|
||||
# mehrere Absätze -> je Absatz ein eigenes <text> an derselben Stelle,
|
||||
# jeweils mit Kritas Transformation
|
||||
parent = parents[text]
|
||||
idx = list(parent).index(text)
|
||||
parent.remove(text)
|
||||
for bi, blk in enumerate(blocks):
|
||||
t2 = ET.Element(text_tag, dict(text.attrib))
|
||||
if text.get("id"):
|
||||
t2.set("id", "%s_a%d" % (text.get("id"), bi + 1))
|
||||
fill(t2, *blk)
|
||||
parent.insert(idx + bi, t2)
|
||||
fixed += 1
|
||||
split += 1
|
||||
return fixed, split, kept
|
||||
|
||||
|
||||
def is_mask(node):
|
||||
return node.type().endswith("mask")
|
||||
|
||||
@@ -314,7 +463,19 @@ class SvgExporter(object):
|
||||
|
||||
tree = ET.ElementTree(root)
|
||||
if hasattr(ET, "indent"):
|
||||
# Text-Inhalte vor dem Einrücken sichern: Whitespace zwischen tspans
|
||||
# wäre bei xml:space="preserve" sichtbarer Text
|
||||
saved = []
|
||||
for t in root.iter(q(SVG_NS, "text")):
|
||||
saved.append((t, t.text))
|
||||
for el in t.iter():
|
||||
if el is not t:
|
||||
saved.append((el, el.text, el.tail))
|
||||
ET.indent(tree, space=" ")
|
||||
for entry in saved:
|
||||
entry[0].text = entry[1]
|
||||
if len(entry) == 3:
|
||||
entry[0].tail = entry[2]
|
||||
os.makedirs(os.path.dirname(self.svg_path) or ".", exist_ok=True)
|
||||
tree.write(self.svg_path, encoding="utf-8", xml_declaration=True)
|
||||
self.log(" -> %s (%d Warnung(en))" % (self.svg_path, self.warnings))
|
||||
@@ -596,6 +757,18 @@ class SvgExporter(object):
|
||||
|
||||
uid = self._uid(name)
|
||||
prefix_ids(src, uid + "_")
|
||||
skip = {q(SVG_NS, "title"), q(SVG_NS, "metadata"), q(SODI_NS, "namedview")}
|
||||
single_shape = len([c for c in src if c.tag not in skip
|
||||
and c.tag != q(SVG_NS, "defs")]) == 1
|
||||
fixed, split, kept = inkscape_text_lines(src)
|
||||
if fixed:
|
||||
self.log(" · „%s“: %d Text(e) in Inkscape-Zeilen umgebaut" % (name, fixed))
|
||||
if split:
|
||||
self.log(" · „%s“: %d Text(e) wegen wechselnder Zeilenabstände in Absätze geteilt"
|
||||
% (name, split))
|
||||
if kept:
|
||||
self.warn("„%s“: %d Text(e) mit Sonderpositionierung unverändert übernommen – "
|
||||
"beim Bearbeiten in Inkscape evtl. Zeilenversatz" % (name, kept))
|
||||
g = ET.SubElement(svg_parent, q(SVG_NS, "g"), {"id": uid})
|
||||
g.set(q(INK_NS, "label"), name)
|
||||
tf = []
|
||||
@@ -608,7 +781,6 @@ class SvgExporter(object):
|
||||
g.set("transform", " ".join(tf))
|
||||
self._apply_common(node, g)
|
||||
|
||||
skip = {q(SVG_NS, "title"), q(SVG_NS, "metadata"), q(SODI_NS, "namedview")}
|
||||
n = 0
|
||||
for child in list(src):
|
||||
if child.tag in skip:
|
||||
@@ -616,6 +788,20 @@ class SvgExporter(object):
|
||||
g.append(child)
|
||||
n += 1
|
||||
self.log(" · Vektorebene „%s“: %d Element(e) übernommen" % (name, n))
|
||||
|
||||
# Eine Ebene mit nur einem Objekt (auch ein in Absätze geteilter Text)
|
||||
# braucht keine eigene Gruppe, sofern die Gruppe nichts Eigenes trägt:
|
||||
# Transformation auf die Objekte übertragen und Gruppe auflösen.
|
||||
if single_shape and g.get("opacity") is None and g.get("style") is None:
|
||||
gtf = g.get("transform")
|
||||
idx = list(svg_parent).index(g)
|
||||
svg_parent.remove(g)
|
||||
for child in list(g):
|
||||
if child.tag != q(SVG_NS, "defs"):
|
||||
ctf = child.get("transform")
|
||||
child.set("transform", gtf + (" " + ctf if ctf else ""))
|
||||
svg_parent.insert(idx, child)
|
||||
idx += 1
|
||||
return True
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user