Files
kanimaji-dart/lib/kanjivg_parser.dart
T
2026-02-21 17:24:59 +09:00

203 lines
5.7 KiB
Dart

import 'package:xml/xml.dart';
import 'svg_parser.dart';
import 'primitives/path.dart';
import 'primitives/point.dart';
/// Enum set in the kvg:position attribute, used by <g> elements in the KanjiVG SVG files.
enum KanjiPathGroupPosition {
bottom,
kamae,
kamaec,
left,
middle,
nyo,
nyoc,
right,
tare,
tarec,
top;
factory KanjiPathGroupPosition.fromString(String s) => switch (s) {
'bottom' => bottom,
'kamae' => kamae,
'kamaec' => kamaec,
'left' => left,
'middle' => middle,
'nyo' => nyo,
'nyoc' => nyoc,
'right' => right,
'tare' => tare,
'tarec' => tarec,
'top' => top,
_ => throw ArgumentError('Invalid position string: $s'),
};
@override
String toString() => switch (this) {
KanjiPathGroupPosition.bottom => 'bottom',
KanjiPathGroupPosition.kamae => 'kamae',
KanjiPathGroupPosition.kamaec => 'kamaec',
KanjiPathGroupPosition.left => 'left',
KanjiPathGroupPosition.middle => 'middle',
KanjiPathGroupPosition.nyo => 'nyo',
KanjiPathGroupPosition.nyoc => 'nyoc',
KanjiPathGroupPosition.right => 'right',
KanjiPathGroupPosition.tare => 'tare',
KanjiPathGroupPosition.tarec => 'tarec',
KanjiPathGroupPosition.top => 'top',
};
}
/// Contents of a \<g> element in the KanjiVG SVG files.
class KanjiPathGroupTreeNode {
final String id;
final List<KanjiPathGroupTreeNode> children;
final String? element;
final String? original;
final KanjiPathGroupPosition? position;
final String? radical;
final int? part;
KanjiPathGroupTreeNode({
required this.id,
this.children = const [],
this.element,
this.original,
this.position,
this.radical,
this.part,
});
}
/// Contents of a <text> element in the StrokeNumber's group in the KanjiVG SVG files
class KanjiStrokeNumber {
final int number;
final Point position;
KanjiStrokeNumber(this.number, this.position);
@override
String toString() => 'KanjiStrokeNumber(number: $number, position: $position)';
}
/// Contents of a <path> element in the KanjiVG SVG files
class KanjiVGPath {
final String id;
final String type;
final Path svgPath;
KanjiVGPath({
required this.id,
required this.type,
required this.svgPath,
});
@override
String toString() => 'KanjiVGPath(id: $id, type: $type, svgPath: $svgPath)';
}
/// Representation of the entire KanjiVG SVG file for a single kanji character
class KanjiVGKanji {
final String character;
final List<Path> paths;
final List<KanjiStrokeNumber> strokeNumbers;
final KanjiPathGroupTreeNode? pathGroups;
KanjiVGKanji({
required this.character,
required this.paths,
required this.strokeNumbers,
this.pathGroups,
});
}
/// Small wrapper returned by the parser. It contains the parsed paths and stroke
/// numbers and the kanji character string.
class KanjiVGItem {
final String character;
final List<KanjiVGPath> paths;
final List<KanjiStrokeNumber> strokeNumbers;
KanjiVGItem({
required this.character,
required this.paths,
required this.strokeNumbers,
});
/// Parse the provided KanjiVG SVG content and return a [KanjiVGItem].
factory KanjiVGItem.parseFromXml(String xmlContent) {
final XmlDocument doc = XmlDocument.parse(xmlContent);
String? _kvgAttr(XmlElement el, String name) {
final fromNs = el.getAttribute(name, namespace: 'http://kanjivg.tagaini.net');
if (fromNs != null) return fromNs;
return el.getAttribute('kvg:$name');
}
XmlElement strokePathsGroup = doc.findElements('svg').first.findElements('g').first;
XmlElement kanjiGroup = strokePathsGroup.findElements('g').first;
final character = _kvgAttr(kanjiGroup, 'element') ?? '';
final List<KanjiVGPath> paths = [];
for (final p in strokePathsGroup.findAllElements('path')) {
final d = p.getAttribute('d');
if (d == null || d.trim().isEmpty) {
continue;
}
final id = p.getAttribute('id') ?? '';
final type = _kvgAttr(p, 'type') ?? p.getAttribute('kvg:type') ?? '';
final svgPath = parsePath(d);
paths.add(KanjiVGPath(id: id, type: type, svgPath: svgPath));
}
final List<KanjiStrokeNumber> strokeNumbers = [];
final strokeNumberGroup = doc.findAllElements('g').firstWhere(
(g) {
final id = g.getAttribute('id') ?? '';
return RegExp(r'^kvg:StrokeNumbers_').hasMatch(id);
},
orElse: () => XmlElement(XmlName('')),
);
if (strokeNumberGroup.name.local != '') {
for (final t in strokeNumberGroup.findAllElements('text')) {
final rawText = t.innerText.trim();
if (rawText.isEmpty) continue;
final numVal = int.tryParse(rawText);
if (numVal == null) continue;
final transform = t.getAttribute('transform') ?? '';
final matches = RegExp(r'[-+]?[0-9]*\\.?[0-9]+').allMatches(transform);
final numbers = matches.map((m) => num.parse(m.group(0)!)).toList();
Point pos;
if (numbers.length >= 2) {
pos = Point(numbers[numbers.length - 2], numbers[numbers.length - 1]);
} else {
// fallback: text element may have x and y attributes instead.
final xs = t.getAttribute('x');
final ys = t.getAttribute('y');
if (xs != null && ys != null) {
try {
pos = Point(num.parse(xs), num.parse(ys));
} catch (_) {
pos = Point.zero;
}
} else {
pos = Point.zero;
}
}
strokeNumbers.add(KanjiStrokeNumber(numVal, pos));
}
}
return KanjiVGItem(
character: character,
paths: paths,
strokeNumbers: strokeNumbers,
);
}
}