David PHAM-VAN

Add support for Emoji

... ... @@ -18,7 +18,7 @@ DART_BIN=$(FLUTTER)/bin/dart
DART_SRC=$(shell find . -name '*.dart')
CLNG_SRC=$(shell find printing/ios printing/macos printing/windows printing/linux printing/android -name '*.cpp' -o -name '*.cc' -o -name '*.m' -o -name '*.h' -o -name '*.java')
SWFT_SRC=$(shell find printing/ios printing/macos -name '*.swift')
FONTS=pdf/open-sans.ttf pdf/open-sans-bold.ttf pdf/roboto.ttf pdf/noto-sans.ttf pdf/genyomintw.ttf pdf/hacen-tunisia.ttf pdf/material.ttf
FONTS=pdf/open-sans.ttf pdf/open-sans-bold.ttf pdf/roboto.ttf pdf/noto-sans.ttf pdf/genyomintw.ttf pdf/hacen-tunisia.ttf pdf/material.ttf pdf/emoji.ttf
COV_PORT=9292
SVG=blend_and_mask blend_mode_devil clip_path clip_path_2 clip_path_2 clip_path_3 clip_path_3 dash_path ellipse empty_defs equation fill-rule-inherit group_composite_opacity group_fill_opacity group_mask group_opacity group_opacity_transform hidden href-fill image image_def implicit_fill_with_opacity linear_gradient linear_gradient_2 linear_gradient_absolute_user_space_translate linear_gradient_percentage_bounding_translate linear_gradient_percentage_user_space_translate linear_gradient_xlink male mask mask_with_gradient mask_with_use mask_with_use2 nested_group opacity_on_path radial_gradient radial_gradient_absolute_user_space_translate radial_gradient_focal radial_gradient_percentage_bounding_translate radial_gradient_percentage_user_space_translate radial_gradient_xlink radial_ref_linear_gradient rect_rrect rect_rrect_no_ry stroke_inherit_circles style_attr text text_2 text_3 use_circles use_circles_def use_emc2 use_fill use_opacity_grid width_height_viewbox flutter_logo emoji_u1f600 text_transform dart new-pause-button new-send-circle new-gif new-camera new-image numeric_25 new-mention new-gif-button new-action-expander new-play-button aa alphachannel Ghostscript_Tiger Firefox_Logo_2017 chess_knight Flag_of_the_United_States
... ... @@ -42,6 +42,9 @@ pdf/genyomintw.ttf:
pdf/material.ttf:
curl -L "https://github.com/google/material-design-icons/raw/master/font/MaterialIcons-Regular.ttf" > $@
pdf/emoji.ttf:
curl -L https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf > $@
demo/assets/logo.svg:
curl -L "http://pigment.github.io/fake-logos/logos/vector/color/auto-speed.svg" > $@
... ...
... ... @@ -5,6 +5,7 @@
- Move files
- Depreciate Font.stringSize
- Implement fallback font
- Implement Emoji support
## 3.6.6
... ...
... ... @@ -68,6 +68,52 @@ class TtfGlyphInfo {
String toString() => 'Glyph $index $compounds';
}
class TtfBitmapInfo {
const TtfBitmapInfo(
this.data,
this.height,
this.width,
this.horiBearingX,
this.horiBearingY,
this.horiAdvance,
this.vertBearingX,
this.vertBearingY,
this.vertAdvance,
this.ascent,
this.descent,
);
final Uint8List data;
final int height;
final int width;
final int horiBearingX;
final int horiBearingY;
final int horiAdvance;
final int vertBearingX;
final int vertBearingY;
final int vertAdvance;
final int ascent;
final int descent;
PdfFontMetrics get metrics {
final coef = 1.0 / height;
return PdfFontMetrics(
bottom: horiBearingY * coef,
left: horiBearingX * coef,
top: horiBearingY * coef - height * coef,
right: horiAdvance * coef,
ascent: ascent * coef,
descent: horiBearingY * coef,
advanceWidth: horiAdvance * coef,
leftBearing: horiBearingX * coef,
);
}
@override
String toString() =>
'Bitmap Glyph ${width}x$height horiBearingX:$horiBearingX horiBearingY:$horiBearingY horiAdvance:$horiAdvance ascender:$ascent descender:$descent';
}
class TtfParser {
TtfParser(ByteData bytes) : bytes = UnmodifiableByteDataView(bytes) {
final numTables = bytes.getUint16(4);
... ... @@ -92,15 +138,18 @@ class TtfParser {
'Unable to find the `cmap` table. This file is not a supported TTF font');
assert(tableOffsets.containsKey(maxp_table),
'Unable to find the `maxp` table. This file is not a supported TTF font');
assert(tableOffsets.containsKey(loca_table),
'Unable to find the `loca` table. This file is not a supported TTF font');
assert(tableOffsets.containsKey(glyf_table),
'Unable to find the `glyf` table. This file is not a supported TTF font');
_parseCMap();
if (tableOffsets.containsKey(loca_table) &&
tableOffsets.containsKey(glyf_table)) {
_parseIndexes();
_parseGlyphs();
}
if (tableOffsets.containsKey(cblc_table) &&
tableOffsets.containsKey(cbdt_table)) {
_parseBitmaps();
}
}
static const String head_table = 'head';
static const String name_table = 'name';
... ... @@ -110,14 +159,17 @@ class TtfParser {
static const String maxp_table = 'maxp';
static const String loca_table = 'loca';
static const String glyf_table = 'glyf';
static const String cblc_table = 'CBLC';
static const String cbdt_table = 'CBDT';
final UnmodifiableByteDataView bytes;
final Map<String, int> tableOffsets = <String, int>{};
final Map<String, int> tableSize = <String, int>{};
final tableOffsets = <String, int>{};
final tableSize = <String, int>{};
final Map<int, int> charToGlyphIndexMap = <int, int>{};
final List<int> glyphOffsets = <int>[];
final Map<int, PdfFontMetrics> glyphInfoMap = <int, PdfFontMetrics>{};
final charToGlyphIndexMap = <int, int>{};
final glyphOffsets = <int>[];
final glyphInfoMap = <int, PdfFontMetrics>{};
final bitmapOffsets = <int, TtfBitmapInfo>{};
int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18);
... ... @@ -145,9 +197,14 @@ class TtfParser {
bool get unicode => bytes.getUint32(0) == 0x10000;
bool get isBitmap => bitmapOffsets.isNotEmpty && glyphOffsets.isEmpty;
// https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
String? getNameID(TtfParserName fontNameID) {
final basePosition = tableOffsets[name_table]!;
final basePosition = tableOffsets[name_table];
if (basePosition == null) {
return null;
}
// final format = bytes.getUint16(basePosition);
final count = bytes.getUint16(basePosition + 2);
final stringOffset = bytes.getUint16(basePosition + 4);
... ... @@ -288,15 +345,15 @@ class TtfParser {
}
void _parseIndexes() {
final basePosition = tableOffsets[loca_table];
final basePosition = tableOffsets[loca_table]!;
final numGlyphs = this.numGlyphs;
if (indexToLocFormat == 0) {
for (var i = 0; i < numGlyphs; i++) {
glyphOffsets.add(bytes.getUint16(basePosition! + i * 2) * 2);
glyphOffsets.add(bytes.getUint16(basePosition + i * 2) * 2);
}
} else {
for (var i = 0; i < numGlyphs; i++) {
glyphOffsets.add(bytes.getUint32(basePosition! + i * 4));
glyphOffsets.add(bytes.getUint32(basePosition + i * 4));
}
}
}
... ... @@ -465,4 +522,92 @@ class TtfParser {
}
return String.fromCharCodes(charCodes);
}
// https://docs.microsoft.com/en-us/typography/opentype/spec/ebdt
void _parseBitmaps() {
final baseOffset = tableOffsets[cblc_table]!;
final pngOffset = tableOffsets[cbdt_table]!;
// CBLC Header
final numSizes = bytes.getUint32(baseOffset + 4);
var bitmapSize = baseOffset + 8;
for (var bitmapSizeIndex = 0;
bitmapSizeIndex < numSizes;
bitmapSizeIndex++) {
// BitmapSize Record
final indexSubTableArrayOffset = baseOffset + bytes.getUint32(bitmapSize);
// final indexTablesSize = bytes.getUint32(bitmapSize + 4);
final numberOfIndexSubTables = bytes.getUint32(bitmapSize + 8);
final ascender = bytes.getInt8(bitmapSize + 12);
final descender = bytes.getInt8(bitmapSize + 13);
// final startGlyphIndex = bytes.getUint16(bitmapSize + 16 + 12 * 2);
// final endGlyphIndex = bytes.getUint16(bitmapSize + 16 + 12 * 2 + 2);
// final ppemX = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 4);
// final ppemY = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 5);
// final bitDepth = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 6);
// final flags = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 7);
var subTableArrayOffset = indexSubTableArrayOffset;
for (var indexSubTable = 0;
indexSubTable < numberOfIndexSubTables;
indexSubTable++) {
// IndexSubTableArray
final firstGlyphIndex = bytes.getUint16(subTableArrayOffset);
final lastGlyphIndex = bytes.getUint16(subTableArrayOffset + 2);
final additionalOffsetToIndexSubtable =
indexSubTableArrayOffset + bytes.getUint32(subTableArrayOffset + 4);
// IndexSubHeader
final indexFormat = bytes.getUint16(additionalOffsetToIndexSubtable);
final imageFormat =
bytes.getUint16(additionalOffsetToIndexSubtable + 2);
final imageDataOffset =
pngOffset + bytes.getUint32(additionalOffsetToIndexSubtable + 4);
if (indexFormat == 1) {
// IndexSubTable1
for (var glyph = firstGlyphIndex; glyph <= lastGlyphIndex; glyph++) {
final sbitOffset = imageDataOffset +
bytes.getUint32(additionalOffsetToIndexSubtable +
(glyph - firstGlyphIndex + 2) * 4);
if (imageFormat == 17) {
final height = bytes.getUint8(sbitOffset);
final width = bytes.getUint8(sbitOffset + 1);
final bearingX = bytes.getInt8(sbitOffset + 2);
final bearingY = bytes.getInt8(sbitOffset + 3);
final advance = bytes.getUint8(sbitOffset + 4);
final dataLen = bytes.getUint32(sbitOffset + 5);
bitmapOffsets[glyph] = TtfBitmapInfo(
bytes.buffer.asUint8List(
bytes.offsetInBytes + sbitOffset + 9,
dataLen,
),
height,
width,
bearingX,
bearingY,
advance,
0,
0,
0,
ascender,
descender);
}
}
}
subTableArrayOffset += 8;
}
bitmapSize += 16 + 12 * 2 + 8;
}
}
TtfBitmapInfo? getBitmap(int charcode) =>
bitmapOffsets[charToGlyphIndexMap[charcode]];
}
... ...
... ... @@ -23,7 +23,7 @@ class PdfUnicodeCmap extends PdfObjectStream {
PdfUnicodeCmap(PdfDocument pdfDocument, this.protect) : super(pdfDocument);
/// List of characters
final List<int> cmap = <int>[0];
final cmap = <int>[];
/// Protects the text from being "seen" by the PDF reader.
final bool protect;
... ...
... ... @@ -24,6 +24,8 @@ import 'annotations.dart';
import 'basic.dart';
import 'document.dart';
import 'geometry.dart';
import 'image.dart';
import 'image_provider.dart';
import 'multi_page.dart';
import 'placeholders.dart';
import 'text_style.dart';
... ... @@ -690,6 +692,25 @@ class RichText extends Widget with SpanningWidget {
_decorations.add(td);
}
InlineSpan _addEmoji({
required TtfBitmapInfo bitmap,
double baseline = 0,
required TextStyle style,
AnnotationBuilder? annotation,
}) {
final metrics = bitmap.metrics * style.fontSize!;
return WidgetSpan(
child: SizedBox(
height: style.fontSize,
child: Image(MemoryImage(bitmap.data)),
),
style: style,
baseline: baseline + metrics.ascent + metrics.descent - metrics.height,
annotation: annotation,
);
}
InlineSpan _addText({
required List<int> text,
int start = 0,
... ... @@ -770,6 +791,19 @@ class RichText extends Widget with SpanningWidget {
for (final fb in style.fontFallback) {
final font = fb.getFont(context);
if (font.isRuneSupported(rune)) {
if (font is PdfTtfFont) {
final bitmap = font.font.getBitmap(rune);
if (bitmap != null) {
spans.add(_addEmoji(
bitmap: bitmap,
style: style,
baseline: span.baseline,
annotation: annotation,
));
found = true;
break;
}
}
spans.add(_addText(
text: [rune],
style: style.copyWith(
... ...
... ... @@ -27,6 +27,7 @@ late Document pdf;
late Font ttf;
late Font ttfBold;
late Font asian;
late Font emoji;
Iterable<TextDecoration> permute(
List<TextDecoration> prefix, List<TextDecoration> remaining) sync* {
... ... @@ -48,6 +49,7 @@ void main() {
ttf = loadFont('open-sans.ttf');
ttfBold = loadFont('open-sans-bold.ttf');
asian = loadFont('genyomintw.ttf');
emoji = loadFont('emoji.ttf');
pdf = Document();
});
... ... @@ -357,6 +359,20 @@ void main() {
);
});
test('Text Widgets Emojis', () {
pdf.addPage(
Page(
build: (Context context) => Text(
'Hello 🐈! Dancing 💃🏃',
style: TextStyle(
fontSize: 30,
fontFallback: [emoji],
),
),
),
);
});
tearDownAll(() async {
final file = File('widgets-text.pdf');
await file.writeAsBytes(await pdf.save());
... ...
... ... @@ -128,8 +128,12 @@ void main(List<String> args) async {
}
for (final entry in <String, String>{
'CupertinoIcons':
'https://github.com/flutter/packages/blob/master/third_party/packages/cupertino_icons/assets/CupertinoIcons.ttf',
'MaterialIcons':
'https://fonts.gstatic.com/s/materialicons/v98/flUhRq6tzZclQEJ-Vdg-IuiaDsNZ.ttf',
'NotoColorEmoji':
'https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf',
}.entries) {
output.writeln('');
output.writeln('/// ${entry.key}');
... ...