Files
kanimaji-dart/lib/kanjivg_parser.dart
2026-02-22 18:19:30 +09:00

213 lines
5.9 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 num;
final Point position;
KanjiStrokeNumber(this.num, this.position);
@override
String toString() =>
'KanjiStrokeNumber(number: $num, 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,
});
/// Helper method to get attributes that may be in the kvg namespace or as `kvg:` prefixed attributes.
static 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');
}
/// Parse the path data from the provided XML document and return a list of [Path]s.
static List<KanjiVGPath> _parsePaths(XmlDocument doc) {
final List<KanjiVGPath> paths = [];
for (final p in doc.findAllElements('path')) {
final d = p.getAttribute('d');
if (d == null || d.trim().isEmpty) {
continue;
}
final svgPath = parsePath(d);
final id = p.getAttribute('id') ?? '';
final type = _kvgAttr(p, 'type') ?? p.getAttribute('kvg:type') ?? '';
paths.add(KanjiVGPath(id: id, type: type, svgPath: svgPath));
}
return paths;
}
/// Parse the stroke number group from the provided XML document and return a list of [KanjiStrokeNumber]s.
static List<KanjiStrokeNumber> _parseStrokeNumbers(XmlDocument doc) {
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 numbers = transform
.replaceAll('matrix(1 0 0 1', '')
.replaceAll(')', '')
.trim()
.split(' ')
.map(num.parse)
.toList();
assert(
numbers.length >= 2,
'Expected at least 2 numbers in transform for stroke number position',
);
Point pos = Point(
numbers[numbers.length - 2],
numbers[numbers.length - 1],
);
strokeNumbers.add(KanjiStrokeNumber(numVal, pos));
}
}
return strokeNumbers;
}
/// Parse the provided KanjiVG SVG content and return a [KanjiVGItem].
factory KanjiVGItem.parseFromXml(String xmlContent) {
final XmlDocument doc = XmlDocument.parse(xmlContent);
XmlElement strokePathsGroup = doc
.findElements('svg')
.first
.findElements('g')
.first;
XmlElement kanjiGroup = strokePathsGroup.findElements('g').first;
final character = _kvgAttr(kanjiGroup, 'element') ?? '';
final paths = _parsePaths(doc);
final strokeNumbers = _parseStrokeNumbers(doc);
return KanjiVGItem(
character: character,
paths: paths,
strokeNumbers: strokeNumbers,
);
}
}