video: ass subtitle format (WIP)

This commit is contained in:
Thibault Deckers 2021-06-21 19:17:07 +09:00
parent 867447832a
commit 2d32b782bc
3 changed files with 89 additions and 52 deletions

View file

@ -4,7 +4,7 @@ import 'package:aves/widgets/viewer/visual/subtitle/style.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AssParser { class AssParser {
static final tagPattern = RegExp(r'(' static final tagPattern = RegExp(r'\*?('
r'1a|2a|3a|4a' r'1a|2a|3a|4a'
r'|1c|2c|3c|4c' r'|1c|2c|3c|4c'
r'|alpha|an|a' r'|alpha|an|a'
@ -31,8 +31,8 @@ class AssParser {
// (<X>,<Y>) // (<X>,<Y>)
static final multiParamPattern = RegExp('\\((.*)\\)'); static final multiParamPattern = RegExp('\\((.*)\\)');
// e.g. m 50 0 b 100 0 100 100 50 100 b 0 100 0 0 50 0 // e.g. m 937.5 472.67 b 937.5 472.67 937.25 501.25 960 501.5 960 501.5 937.5 500.33 937.5 529.83
static final pathPattern = RegExp(r'([mnlbspc])([\s\d]+)'); static final pathPattern = RegExp(r'([mnlbspc])([.\s\d]+)');
static const noBreakSpace = '\u00A0'; static const noBreakSpace = '\u00A0';
@ -50,9 +50,10 @@ class AssParser {
final matches = RegExp(r'{(.*?)}').allMatches(text); final matches = RegExp(r'{(.*?)}').allMatches(text);
matches.forEach((m) { matches.forEach((m) {
if (i != m.start) { if (i != m.start) {
final spanText = extraStyle.drawingPaths?.isNotEmpty == true ? null : _replaceChars(text.substring(i, m.start));
spans.add(StyledSubtitleSpan( spans.add(StyledSubtitleSpan(
textSpan: TextSpan( textSpan: TextSpan(
text: extraStyle.drawingPaths?.isNotEmpty == true ? null : _replaceChars(text.substring(i, m.start)), text: spanText,
style: textStyle, style: textStyle,
), ),
extraStyle: extraStyle, extraStyle: extraStyle,
@ -230,13 +231,14 @@ class AssParser {
// \p drawing paths // \p drawing paths
final scale = int.tryParse(param); final scale = int.tryParse(param);
if (scale != null) { if (scale != null) {
extraStyle = extraStyle.copyWith( if (scale > 0) {
drawingPaths: scale > 0 final start = m.end;
? _parsePaths( final end = text.indexOf('{', start);
text.substring(m.end), final commands = text.substring(start, end == -1 ? null : end);
scale, extraStyle = extraStyle.copyWith(drawingPaths: _parsePaths(commands, scale));
) } else {
: null); extraStyle = extraStyle.copyWith(drawingPaths: null);
}
} }
break; break;
} }
@ -307,9 +309,10 @@ class AssParser {
}); });
}); });
if (i != text.length) { if (i != text.length) {
final spanText = extraStyle.drawingPaths?.isNotEmpty == true ? null : _replaceChars(text.substring(i));
spans.add(StyledSubtitleSpan( spans.add(StyledSubtitleSpan(
textSpan: TextSpan( textSpan: TextSpan(
text: extraStyle.drawingPaths?.isNotEmpty == true ? null : _replaceChars(text.substring(i)), text: spanText,
style: textStyle, style: textStyle,
), ),
extraStyle: extraStyle, extraStyle: extraStyle,

View file

@ -9,11 +9,11 @@ class SubtitleStyle with Diagnosticable {
final double? borderWidth, edgeBlur, rotationX, rotationY, rotationZ, scaleX, scaleY, shearX, shearY; final double? borderWidth, edgeBlur, rotationX, rotationY, rotationZ, scaleX, scaleY, shearX, shearY;
final List<Path>? drawingPaths; final List<Path>? drawingPaths;
bool get rotating => (rotationX ?? 0) > 0 || (rotationY ?? 0) > 0 || (rotationZ ?? 0) > 0; bool get rotating => (rotationX ?? 0) != 0 || (rotationY ?? 0) != 0 || (rotationZ ?? 0) != 0;
bool get scaling => (scaleX ?? 0) > 0 || (scaleY ?? 0) > 0; bool get scaling => (scaleX ?? 1) != 1 || (scaleY ?? 1) != 1;
bool get shearing => (shearX ?? 0) > 0 || (shearY ?? 0) > 0; bool get shearing => (shearX ?? 0) != 0 || (shearY ?? 0) != 0;
const SubtitleStyle({ const SubtitleStyle({
this.hAlign, this.hAlign,

View file

@ -73,11 +73,20 @@ class VideoSubtitles extends StatelessWidget {
if (text == null) return const SizedBox(); if (text == null) return const SizedBox();
if (debugMode) { if (debugMode) {
return Center( return Padding(
child: OutlinedText( padding: const EdgeInsets.only(top: 100.0),
textSpans: [TextSpan(text: text)], child: Align(
outlineWidth: 1, alignment: Alignment.topLeft,
outlineColor: Colors.black, child: OutlinedText(
textSpans: [
TextSpan(
text: text,
style: const TextStyle(fontSize: 14),
)
],
outlineWidth: 1,
outlineColor: Colors.black,
),
), ),
); );
} }
@ -90,26 +99,45 @@ class VideoSubtitles extends StatelessWidget {
return Stack( return Stack(
children: byExtraStyle.entries.map((kv) { children: byExtraStyle.entries.map((kv) {
final extraStyle = kv.key; final extraStyle = kv.key;
final spans = kv.value.map((v) => v.textSpan).toList(); final spans = kv.value.map((v) {
final span = v.textSpan;
final style = span.style;
return position != null && style != null
? TextSpan(
text: span.text,
style: style.copyWith(
shadows: style.shadows
?.map((v) => Shadow(
color: v.color,
offset: v.offset * viewScale,
blurRadius: v.blurRadius * viewScale,
))
.toList()),
)
: span;
}).toList();
final drawingPaths = extraStyle.drawingPaths; final drawingPaths = extraStyle.drawingPaths;
final outlineColor = extraStyle.borderColor ?? Colors.black; Widget child;
var child = drawingPaths != null if (drawingPaths != null) {
? CustomPaint( child = CustomPaint(
painter: SubtitlePathPainter( painter: SubtitlePathPainter(
paths: drawingPaths, paths: drawingPaths,
scale: viewScale, scale: viewScale,
fillColor: spans.firstOrNull?.style?.color ?? Colors.white, fillColor: spans.firstOrNull?.style?.color ?? Colors.white,
strokeColor: outlineColor, strokeColor: extraStyle.borderColor,
), ),
) );
: OutlinedText( } else {
textSpans: spans, final outlineWidth = extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1);
outlineWidth: extraStyle.borderWidth ?? (extraStyle.edgeBlur != null ? 2 : 1), child = OutlinedText(
outlineColor: outlineColor, textSpans: spans,
outlineBlurSigma: extraStyle.edgeBlur ?? 0, outlineWidth: outlineWidth * (position != null ? viewScale : 1),
textAlign: extraStyle.hAlign ?? TextAlign.center, outlineColor: extraStyle.borderColor ?? Colors.black,
); outlineBlurSigma: extraStyle.edgeBlur ?? 0,
textAlign: extraStyle.hAlign ?? TextAlign.center,
);
}
var transform = Matrix4.identity(); var transform = Matrix4.identity();
@ -220,28 +248,34 @@ class VideoSubtitles extends StatelessWidget {
class SubtitlePathPainter extends CustomPainter { class SubtitlePathPainter extends CustomPainter {
final List<Path> paths; final List<Path> paths;
final double scale; final double scale;
final Color fillColor, strokeColor; final Paint? fillPaint, strokePaint;
const SubtitlePathPainter({ SubtitlePathPainter({
required this.paths, required this.paths,
required this.scale, required this.scale,
required this.fillColor, required Color? fillColor,
required this.strokeColor, required Color? strokeColor,
}); }) : fillPaint = fillColor != null
? (Paint()
..style = PaintingStyle.fill
..color = fillColor)
: null,
strokePaint = strokeColor != null
? (Paint()
..style = PaintingStyle.stroke
..color = strokeColor)
: null;
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final fillPaint = Paint()
..style = PaintingStyle.fill
..color = fillColor;
final strokePaint = Paint()
..style = PaintingStyle.stroke
..color = strokeColor;
canvas.scale(scale, scale); canvas.scale(scale, scale);
paths.forEach((path) { paths.forEach((path) {
canvas.drawPath(path, fillPaint); if (fillPaint != null) {
canvas.drawPath(path, strokePaint); canvas.drawPath(path, fillPaint!);
}
if (strokePaint != null) {
canvas.drawPath(path, strokePaint!);
}
}); });
} }