203 lines
5.7 KiB
Dart
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,
|
|
);
|
|
}
|
|
}
|