David PHAM-VAN

Implement fallback font

... ... @@ -4,6 +4,7 @@
- Move files
- Depreciate Font.stringSize
- Implement fallback font
## 3.6.6
... ...
... ... @@ -161,6 +161,9 @@ See https://github.com/DavBfr/dart_pdf/wiki/Fonts-Management
/// Calculate the [PdfFontMetrics] for this glyph
PdfFontMetrics glyphMetrics(int charCode);
/// is this Rune supported by this font
bool isRuneSupported(int charCode);
/// Calculate the [PdfFontMetrics] for this string
PdfFontMetrics stringMetrics(String s, {double letterSpacing = 0}) {
if (s.isEmpty) {
... ...
... ... @@ -188,4 +188,9 @@ class PdfTtfFont extends PdfFont {
final metrics = bytes.map(glyphMetrics);
return PdfFontMetrics.append(metrics, letterSpacing: letterSpacing);
}
@override
bool isRuneSupported(int charCode) {
return font.charToGlyphIndexMap.containsKey(charCode);
}
}
... ...
... ... @@ -64,6 +64,11 @@ class PdfType1Font extends PdfFont {
@override
PdfFontMetrics glyphMetrics(int charCode) {
if (!isRuneSupported(charCode)) {
throw Exception(
'Unable to display U+${charCode.toRadixString(16)} with $fontName');
}
return PdfFontMetrics(
left: 0,
top: descent,
... ... @@ -72,4 +77,9 @@ class PdfType1Font extends PdfFont {
: PdfFont.defaultGlyphWidth,
bottom: ascent);
}
@override
bool isRuneSupported(int charCode) {
return charCode >= 0x00 && charCode <= 0xff;
}
}
... ...
... ... @@ -66,7 +66,7 @@ class SvgText extends SvgOperation {
final font = painter.getFontCache(
_brush.fontFamily!, _brush.fontStyle!, _brush.fontWeight!)!;
final pdfFont = font.getFont(Context(document: painter.document))!;
final pdfFont = font.getFont(Context(document: painter.document));
final metrics = pdfFont.stringMetrics(text) * _brush.fontSize!.sizeValue;
offset = PdfPoint((x ?? offset.x) + dx, (y ?? offset.y) + dy);
... ...
... ... @@ -392,7 +392,7 @@ class AnnotationTextField extends AnnotationBuilder {
fieldFlags: fieldFlags,
value: value,
defaultValue: defaultValue,
font: _textStyle.font!.getFont(context)!,
font: _textStyle.font!.getFont(context),
fontSize: _textStyle.fontSize!,
textColor: _textStyle.color!,
),
... ...
... ... @@ -94,7 +94,7 @@ class _BarcodeWidget extends Widget {
final font = textStyle!.font!.getFont(context);
for (final text in textList) {
final metrics = font!.stringMetrics(text.text);
final metrics = font.stringMetrics(text.text);
final top = box!.top -
text.top -
... ...
... ... @@ -43,7 +43,7 @@ enum Type1Fonts {
class Font {
Font() : font = null;
Font.type1(Type1Fonts this.font);
Font.type1(this.font);
factory Font.courier() => Font.type1(Type1Fonts.courier);
factory Font.courierBold() => Font.type1(Type1Fonts.courierBold);
... ... @@ -127,13 +127,13 @@ class Font {
PdfFont? _pdfFont;
PdfFont? getFont(Context context) {
PdfFont getFont(Context context) {
if (_pdfFont == null || _pdfFont!.pdfDocument != context.document) {
final pdfDocument = context.document;
_pdfFont = buildFont(pdfDocument);
}
return _pdfFont;
return _pdfFont!;
}
@override
... ...
... ... @@ -278,7 +278,7 @@ class TextField extends StatelessWidget {
fieldFlags: fieldFlags,
value: value,
defaultValue: defaultValue,
font: _textStyle.font!.getFont(context)!,
font: _textStyle.font!.getFont(context),
fontSize: _textStyle.fontSize!,
textColor: _textStyle.color!,
);
... ...
... ... @@ -21,9 +21,11 @@ import 'package:pdf/pdf.dart';
import 'package:pdf/src/pdf/font/arabic.dart' as arabic;
import 'annotations.dart';
import 'basic.dart';
import 'document.dart';
import 'geometry.dart';
import 'multi_page.dart';
import 'placeholders.dart';
import 'text_style.dart';
import 'theme.dart';
import 'widget.dart';
... ... @@ -170,7 +172,7 @@ class _TextDecoration {
0.05);
if (style.decoration!.contains(TextDecoration.underline)) {
final base = -font!.descent * style.fontSize! * textScaleFactor / 2;
final base = -font.descent * style.fontSize! * textScaleFactor / 2;
context.canvas.drawLine(
globalBox!.x + box!.left,
... ... @@ -209,7 +211,7 @@ class _TextDecoration {
}
if (style.decoration!.contains(TextDecoration.lineThrough)) {
final base = (1 - font!.descent) * style.fontSize! * textScaleFactor / 2;
final base = (1 - font.descent) * style.fontSize! * textScaleFactor / 2;
context.canvas.drawLine(
globalBox!.x + box!.left,
globalBox.top + box.bottom + base,
... ... @@ -281,7 +283,7 @@ class _Word extends _Span {
PdfPoint point,
) {
context.canvas.drawString(
style.font!.getFont(context)!,
style.font!.getFont(context),
style.fontSize! * textScaleFactor,
text,
point.x + offset.x,
... ... @@ -316,10 +318,12 @@ class _Word extends _Span {
}
class _WidgetSpan extends _Span {
_WidgetSpan(this.widget, TextStyle style) : super(style);
_WidgetSpan(this.widget, TextStyle style, this.baseline) : super(style);
final Widget widget;
final double baseline;
@override
double get left => 0;
... ... @@ -365,11 +369,21 @@ class _WidgetSpan extends _Span {
double textScaleFactor,
PdfRect? globalBox,
) {
const deb = 5;
context.canvas
..setLineWidth(.5)
..drawRect(
globalBox!.x + offset.x, globalBox.top + offset.y, width, height)
..setStrokeColor(PdfColors.orange)
..strokePath()
..drawLine(
globalBox.x + offset.x - deb,
globalBox.top + offset.y - baseline,
globalBox.x + offset.x + width + deb,
globalBox.top + offset.y - baseline,
)
..setStrokeColor(PdfColors.deepPurple)
..strokePath();
}
}
... ... @@ -382,14 +396,24 @@ typedef _VisitorCallback = bool Function(
@immutable
abstract class InlineSpan {
const InlineSpan({this.style, this.baseline, this.annotation});
const InlineSpan({
this.style,
required this.baseline,
this.annotation,
});
final TextStyle? style;
final double? baseline;
final double baseline;
final AnnotationBuilder? annotation;
InlineSpan copyWith({
TextStyle? style,
double? baseline,
AnnotationBuilder? annotation,
});
String toPlainText() {
final buffer = StringBuffer();
visitChildren((
... ... @@ -424,6 +448,19 @@ class WidgetSpan extends InlineSpan {
/// The widget to embed inline within text.
final Widget child;
@override
InlineSpan copyWith({
TextStyle? style,
double? baseline,
AnnotationBuilder? annotation,
}) =>
WidgetSpan(
child: child,
style: style ?? this.style,
baseline: baseline ?? this.baseline,
annotation: annotation ?? this.annotation,
);
/// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk.
@override
bool visitChildren(
... ... @@ -452,6 +489,20 @@ class TextSpan extends InlineSpan {
final List<InlineSpan>? children;
@override
InlineSpan copyWith({
TextStyle? style,
double? baseline,
AnnotationBuilder? annotation,
}) =>
TextSpan(
style: style ?? this.style,
text: text,
baseline: baseline ?? this.baseline,
children: children,
annotation: annotation ?? this.annotation,
);
@override
bool visitChildren(
_VisitorCallback visitor,
TextStyle? parentStyle,
... ... @@ -624,6 +675,8 @@ class RichText extends Widget with SpanningWidget {
var _mustClip = false;
List<InlineSpan>? _preprocessed;
void _appendDecoration(bool append, _TextDecoration td) {
if (append && _decorations.isNotEmpty) {
final last = _decorations.last;
... ... @@ -637,6 +690,132 @@ class RichText extends Widget with SpanningWidget {
_decorations.add(td);
}
InlineSpan _addText({
required List<int> text,
int start = 0,
int? end,
double baseline = 0,
required TextStyle style,
AnnotationBuilder? annotation,
}) {
return TextSpan(
text: String.fromCharCodes(text, start, end),
style: style,
baseline: baseline,
annotation: annotation,
);
}
InlineSpan _addPlaceholder({
double baseline = 0,
required TextStyle style,
AnnotationBuilder? annotation,
}) {
return WidgetSpan(
child: SizedBox(
height: style.fontSize,
width: style.fontSize! / 2,
child: Placeholder(
color: style.color!,
strokeWidth: 1,
),
),
style: style,
baseline: baseline,
annotation: annotation,
);
}
/// Check available characters in the fonts
/// use fallback if needed and replace emojis
List<InlineSpan> _preprocessSpans(Context context) {
final theme = Theme.of(context);
final defaultstyle = theme.defaultTextStyle;
final spans = <InlineSpan>[];
text.visitChildren((
InlineSpan span,
TextStyle? style,
AnnotationBuilder? annotation,
) {
if (span is! TextSpan) {
spans.add(span.copyWith(style: style, annotation: annotation));
return true;
}
if (span.text == null) {
return true;
}
final font = style!.font!.getFont(context);
var text = span.text!.runes.toList();
for (var index = 0; index < text.length; index++) {
final rune = text[index];
if (rune == 0x0a) {
continue;
}
if (!font.isRuneSupported(rune)) {
if (index > 0) {
spans.add(_addText(
text: text,
end: index,
style: style,
baseline: span.baseline,
annotation: annotation,
));
}
var found = false;
for (final fb in style.fontFallback) {
final font = fb.getFont(context);
if (font.isRuneSupported(rune)) {
spans.add(_addText(
text: [rune],
style: style.copyWith(
font: fb,
fontNormal: fb,
fontBold: fb,
fontBoldItalic: fb,
fontItalic: fb,
),
baseline: span.baseline,
annotation: annotation,
));
found = true;
break;
}
}
if (!found) {
spans.add(_addPlaceholder(
style: style,
baseline: span.baseline,
annotation: annotation,
));
assert(() {
print(
'Unable to find a font to draw "${String.fromCharCode(rune)}" (U+${rune.toRadixString(16)}) try to provide a TextStyle.fontFallback');
return true;
}());
}
text = text.sublist(index + 1);
index = -1;
}
}
spans.add(_addText(
text: text,
style: style,
baseline: span.baseline,
annotation: annotation,
));
return true;
}, defaultstyle, null);
return spans;
}
@override
void layout(Context context, BoxConstraints constraints,
{bool parentUsesSize = false}) {
... ... @@ -644,7 +823,6 @@ class RichText extends Widget with SpanningWidget {
_decorations.clear();
final theme = Theme.of(context);
final defaultstyle = theme.defaultTextStyle;
final _softWrap = softWrap ?? theme.softWrap;
final _maxLines = maxLines ?? theme.maxLines;
_textAlign = textAlign ?? theme.textAlign;
... ... @@ -669,228 +847,232 @@ class RichText extends Widget with SpanningWidget {
var spanStart = 0;
var overflow = false;
text.visitChildren((
InlineSpan span,
TextStyle? style,
AnnotationBuilder? annotation,
) {
if (span is TextSpan) {
if (span.text == null) {
return true;
}
_preprocessed ??= _preprocessSpans(context);
final font = style!.font!.getFont(context)!;
void _buildLines() {
for (final span in _preprocessed!) {
final style = span.style;
final annotation = span.annotation;
final space =
font.stringMetrics(' ') * (style.fontSize! * textScaleFactor);
if (span is TextSpan) {
if (span.text == null) {
continue;
}
final spanLines = (_textDirection == TextDirection.rtl
? arabic.convert(span.text!)
: span.text)!
.split('\n');
final font = style!.font!.getFont(context);
for (var line = 0; line < spanLines.length; line++) {
final words = spanLines[line].split(RegExp(r'\s'));
for (var index = 0; index < words.length; index++) {
final word = words[index];
final space =
font.stringMetrics(' ') * (style.fontSize! * textScaleFactor);
if (word.isEmpty) {
offsetX += space.advanceWidth * style.wordSpacing! +
style.letterSpacing!;
continue;
}
final spanLines = (_textDirection == TextDirection.rtl
? arabic.convert(span.text!)
: span.text)!
.split('\n');
final metrics = font.stringMetrics(word,
letterSpacing: style.letterSpacing! /
(style.fontSize! * textScaleFactor)) *
(style.fontSize! * textScaleFactor);
if (_softWrap &&
offsetX + metrics.width > constraintWidth + 0.00001) {
if (spanCount > 0 && metrics.width <= constraintWidth) {
overflow = true;
lines.add(_Line(
this,
spanStart,
spanCount,
bottom,
offsetX -
space.advanceWidth * style.wordSpacing! -
style.letterSpacing!,
_textDirection,
true,
));
spanStart += spanCount;
spanCount = 0;
offsetX = 0.0;
offsetY += bottom - top;
top = 0;
bottom = 0;
for (var line = 0; line < spanLines.length; line++) {
final words = spanLines[line].split(RegExp(r'\s'));
for (var index = 0; index < words.length; index++) {
final word = words[index];
if (_maxLines != null && lines.length >= _maxLines) {
return false;
}
if (word.isEmpty) {
offsetX += space.advanceWidth * style.wordSpacing! +
style.letterSpacing!;
continue;
}
if (offsetY > constraintHeight) {
return false;
final metrics = font.stringMetrics(word,
letterSpacing: style.letterSpacing! /
(style.fontSize! * textScaleFactor)) *
(style.fontSize! * textScaleFactor);
if (_softWrap &&
offsetX + metrics.width > constraintWidth + 0.00001) {
if (spanCount > 0 && metrics.width <= constraintWidth) {
overflow = true;
lines.add(_Line(
this,
spanStart,
spanCount,
bottom,
offsetX -
space.advanceWidth * style.wordSpacing! -
style.letterSpacing!,
_textDirection,
true,
));
spanStart += spanCount;
spanCount = 0;
offsetX = 0.0;
offsetY += bottom - top;
top = 0;
bottom = 0;
if (_maxLines != null && lines.length >= _maxLines) {
return;
}
if (offsetY > constraintHeight) {
return;
}
offsetY += style.lineSpacing! * textScaleFactor;
} else {
// One word Overflow, try to split it.
final pos = _splitWord(word, font, style, constraintWidth);
if (pos < word.length) {
words[index] = word.substring(0, pos);
words.insert(index + 1, word.substring(pos));
// Try again
index--;
continue;
}
}
}
offsetY += style.lineSpacing! * textScaleFactor;
} else {
// One word Overflow, try to split it.
final pos = _splitWord(word, font, style, constraintWidth);
final baseline = span.baseline * textScaleFactor;
final mt = tightBounds ? metrics.top : metrics.descent;
final mb = tightBounds ? metrics.bottom : metrics.ascent;
top = math.min(top, mt + baseline);
bottom = math.max(bottom, mb + baseline);
if (pos < word.length) {
words[index] = word.substring(0, pos);
words.insert(index + 1, word.substring(pos));
final wd = _Word(
word,
style,
metrics,
);
wd.offset = PdfPoint(offsetX, -offsetY + baseline);
_spans.add(wd);
spanCount++;
_appendDecoration(
spanCount > 1,
_TextDecoration(
style,
annotation,
_spans.length - 1,
_spans.length - 1,
),
);
offsetX += metrics.advanceWidth +
space.advanceWidth * style.wordSpacing! +
style.letterSpacing!;
}
// Try again
index--;
continue;
}
if (line < spanLines.length - 1) {
lines.add(_Line(
this,
spanStart,
spanCount,
bottom,
offsetX -
space.advanceWidth * style.wordSpacing! -
style.letterSpacing!,
_textDirection,
false,
));
spanStart += spanCount;
offsetX = 0.0;
if (spanCount > 0) {
offsetY += bottom - top;
} else {
offsetY += space.ascent + space.descent;
}
}
top = 0;
bottom = 0;
spanCount = 0;
final baseline = span.baseline! * textScaleFactor;
final mt = tightBounds ? metrics.top : metrics.descent;
final mb = tightBounds ? metrics.bottom : metrics.ascent;
top = math.min(top, mt + baseline);
bottom = math.max(bottom, mb + baseline);
if (_maxLines != null && lines.length >= _maxLines) {
return;
}
final wd = _Word(
word,
style,
metrics,
);
wd.offset = PdfPoint(offsetX, -offsetY + baseline);
_spans.add(wd);
spanCount++;
_appendDecoration(
spanCount > 1,
_TextDecoration(
style,
annotation,
_spans.length - 1,
_spans.length - 1,
),
);
offsetX += metrics.advanceWidth +
space.advanceWidth * style.wordSpacing! +
style.letterSpacing!;
if (offsetY > constraintHeight) {
return;
}
offsetY += style.lineSpacing! * textScaleFactor;
}
}
if (line < spanLines.length - 1) {
offsetX -=
space.advanceWidth * style.wordSpacing! - style.letterSpacing!;
} else if (span is WidgetSpan) {
span.child.layout(
context,
BoxConstraints(
maxWidth: constraintWidth,
maxHeight: constraintHeight,
));
final ws = _WidgetSpan(
span.child,
style!,
span.baseline,
);
if (offsetX + ws.width > constraintWidth && spanCount > 0) {
overflow = true;
lines.add(_Line(
this,
spanStart,
spanCount,
bottom,
offsetX -
space.advanceWidth * style.wordSpacing! -
style.letterSpacing!,
offsetX,
_textDirection,
false,
true,
));
spanStart += spanCount;
spanCount = 0;
offsetX = 0.0;
if (spanCount > 0) {
offsetY += bottom - top;
} else {
offsetY += space.ascent + space.descent;
if (_maxLines != null && lines.length > _maxLines) {
return;
}
offsetX = 0.0;
offsetY += bottom - top;
top = 0;
bottom = 0;
spanCount = 0;
if (_maxLines != null && lines.length >= _maxLines) {
return false;
}
if (offsetY > constraintHeight) {
return false;
return;
}
offsetY += style.lineSpacing! * textScaleFactor;
}
}
offsetX -=
space.advanceWidth * style.wordSpacing! - style.letterSpacing!;
} else if (span is WidgetSpan) {
span.child.layout(
context,
BoxConstraints(
maxWidth: constraintWidth,
maxHeight: constraintHeight,
));
final ws = _WidgetSpan(
span.child,
style!,
);
if (offsetX + ws.width > constraintWidth && spanCount > 0) {
overflow = true;
lines.add(_Line(
this,
spanStart,
spanCount,
final baseline = span.baseline * textScaleFactor;
top = math.min(top, baseline);
bottom = math.max(
bottom,
offsetX,
_textDirection,
true,
));
spanStart += spanCount;
spanCount = 0;
if (_maxLines != null && lines.length > _maxLines) {
return false;
}
ws.height + baseline,
);
offsetX = 0.0;
offsetY += bottom - top;
top = 0;
bottom = 0;
ws.offset = PdfPoint(offsetX, -offsetY + baseline);
_spans.add(ws);
spanCount++;
if (offsetY > constraintHeight) {
return false;
}
_appendDecoration(
spanCount > 1,
_TextDecoration(
style,
annotation,
_spans.length - 1,
_spans.length - 1,
),
);
offsetY += style.lineSpacing! * textScaleFactor;
offsetX += ws.left + ws.width;
}
final baseline = span.baseline! * textScaleFactor;
top = math.min(top, baseline);
bottom = math.max(
bottom,
ws.height + baseline,
);
ws.offset = PdfPoint(offsetX, -offsetY + baseline);
_spans.add(ws);
spanCount++;
_appendDecoration(
spanCount > 1,
_TextDecoration(
style,
annotation,
_spans.length - 1,
_spans.length - 1,
),
);
offsetX += ws.left + ws.width;
}
}
return true;
}, defaultstyle, null);
_buildLines();
if (spanCount > 0) {
lines.add(_Line(
... ...
... ... @@ -111,6 +111,7 @@ class TextStyle {
Font? fontBold,
Font? fontItalic,
Font? fontBoldItalic,
this.fontFallback = const [],
this.fontSize,
this.fontWeight,
this.fontStyle,
... ... @@ -165,6 +166,7 @@ class TextStyle {
fontBold: Font.helveticaBold(),
fontItalic: Font.helveticaOblique(),
fontBoldItalic: Font.helveticaBoldOblique(),
fontFallback: const [],
fontSize: _defaultFontSize,
fontWeight: FontWeight.normal,
fontStyle: FontStyle.normal,
... ... @@ -192,7 +194,10 @@ class TextStyle {
final Font? fontBoldItalic;
// font height, in pdf unit
/// The ordered list of font to fall back on when a glyph cannot be found in a higher priority font.
final List<Font> fontFallback;
/// font height, in pdf unit
final double? fontSize;
/// The typeface thickness to use when painting the text (e.g., bold).
... ... @@ -233,6 +238,7 @@ class TextStyle {
Font? fontBold,
Font? fontItalic,
Font? fontBoldItalic,
List<Font>? fontFallback,
double? fontSize,
FontWeight? fontWeight,
FontStyle? fontStyle,
... ... @@ -255,6 +261,7 @@ class TextStyle {
fontBold: fontBold ?? this.fontBold,
fontItalic: fontItalic ?? this.fontItalic,
fontBoldItalic: fontBoldItalic ?? this.fontBoldItalic,
fontFallback: fontFallback ?? this.fontFallback,
fontSize: fontSize ?? this.fontSize,
fontWeight: fontWeight ?? this.fontWeight,
fontStyle: fontStyle ?? this.fontStyle,
... ... @@ -339,6 +346,7 @@ class TextStyle {
fontBold: other.fontBold,
fontItalic: other.fontItalic,
fontBoldItalic: other.fontBoldItalic,
fontFallback: [...other.fontFallback, ...fontFallback],
fontSize: other.fontSize,
fontWeight: other.fontWeight,
fontStyle: other.fontStyle,
... ...
... ... @@ -23,6 +23,8 @@ import 'text.dart';
import 'text_style.dart';
import 'widget.dart';
typedef DefaultThemeDataBuilder = ThemeData Function();
@immutable
class ThemeData extends Inherited {
factory ThemeData({
... ... @@ -100,6 +102,7 @@ class ThemeData extends Inherited {
Font? italic,
Font? boldItalic,
Font? icons,
List<Font>? fontFallback,
}) {
final defaultStyle = TextStyle.defaultStyle().copyWith(
font: base,
... ... @@ -107,6 +110,7 @@ class ThemeData extends Inherited {
fontBold: bold,
fontItalic: italic,
fontBoldItalic: boldItalic,
fontFallback: fontFallback,
);
final fontSize = defaultStyle.fontSize!;
... ... @@ -130,7 +134,34 @@ class ThemeData extends Inherited {
);
}
factory ThemeData.base() => ThemeData.withFont();
factory ThemeData.base() =>
buildThemeData == null ? ThemeData.withFont() : buildThemeData!();
static DefaultThemeDataBuilder? buildThemeData;
final TextStyle defaultTextStyle;
final TextStyle paragraphStyle;
final TextStyle header0;
final TextStyle header1;
final TextStyle header2;
final TextStyle header3;
final TextStyle header4;
final TextStyle header5;
final TextStyle bulletStyle;
final TextStyle tableHeader;
final TextStyle tableCell;
final TextAlign textAlign;
final bool softWrap;
final int? maxLines;
final TextOverflow overflow;
final IconThemeData iconTheme;
ThemeData copyWith({
TextStyle? defaultTextStyle,
... ... @@ -168,30 +199,6 @@ class ThemeData extends Inherited {
maxLines: maxLines ?? this.maxLines,
iconTheme: iconTheme ?? this.iconTheme,
);
final TextStyle defaultTextStyle;
final TextStyle paragraphStyle;
final TextStyle header0;
final TextStyle header1;
final TextStyle header2;
final TextStyle header3;
final TextStyle header4;
final TextStyle header5;
final TextStyle bulletStyle;
final TextStyle tableHeader;
final TextStyle tableCell;
final TextAlign textAlign;
final bool softWrap;
final int? maxLines;
final TextOverflow overflow;
final IconThemeData iconTheme;
}
class Theme extends StatelessWidget {
... ...
... ... @@ -24,9 +24,9 @@ import 'package:test/test.dart';
import 'utils.dart';
late Document pdf;
Font? ttf;
Font? ttfBold;
Font? asian;
late Font ttf;
late Font ttfBold;
late Font asian;
Iterable<TextDecoration> permute(
List<TextDecoration> prefix, List<TextDecoration> remaining) sync* {
... ...
No preview for this file type