David PHAM-VAN

Add support for Emoji

@@ -18,7 +18,7 @@ DART_BIN=$(FLUTTER)/bin/dart @@ -18,7 +18,7 @@ DART_BIN=$(FLUTTER)/bin/dart
18 DART_SRC=$(shell find . -name '*.dart') 18 DART_SRC=$(shell find . -name '*.dart')
19 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') 19 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')
20 SWFT_SRC=$(shell find printing/ios printing/macos -name '*.swift') 20 SWFT_SRC=$(shell find printing/ios printing/macos -name '*.swift')
21 -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 21 +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
22 COV_PORT=9292 22 COV_PORT=9292
23 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 23 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
24 24
@@ -42,6 +42,9 @@ pdf/genyomintw.ttf: @@ -42,6 +42,9 @@ pdf/genyomintw.ttf:
42 pdf/material.ttf: 42 pdf/material.ttf:
43 curl -L "https://github.com/google/material-design-icons/raw/master/font/MaterialIcons-Regular.ttf" > $@ 43 curl -L "https://github.com/google/material-design-icons/raw/master/font/MaterialIcons-Regular.ttf" > $@
44 44
  45 +pdf/emoji.ttf:
  46 + curl -L https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf > $@
  47 +
45 demo/assets/logo.svg: 48 demo/assets/logo.svg:
46 curl -L "http://pigment.github.io/fake-logos/logos/vector/color/auto-speed.svg" > $@ 49 curl -L "http://pigment.github.io/fake-logos/logos/vector/color/auto-speed.svg" > $@
47 50
@@ -5,6 +5,7 @@ @@ -5,6 +5,7 @@
5 - Move files 5 - Move files
6 - Depreciate Font.stringSize 6 - Depreciate Font.stringSize
7 - Implement fallback font 7 - Implement fallback font
  8 +- Implement Emoji support
8 9
9 ## 3.6.6 10 ## 3.6.6
10 11
@@ -68,6 +68,52 @@ class TtfGlyphInfo { @@ -68,6 +68,52 @@ class TtfGlyphInfo {
68 String toString() => 'Glyph $index $compounds'; 68 String toString() => 'Glyph $index $compounds';
69 } 69 }
70 70
  71 +class TtfBitmapInfo {
  72 + const TtfBitmapInfo(
  73 + this.data,
  74 + this.height,
  75 + this.width,
  76 + this.horiBearingX,
  77 + this.horiBearingY,
  78 + this.horiAdvance,
  79 + this.vertBearingX,
  80 + this.vertBearingY,
  81 + this.vertAdvance,
  82 + this.ascent,
  83 + this.descent,
  84 + );
  85 +
  86 + final Uint8List data;
  87 + final int height;
  88 + final int width;
  89 + final int horiBearingX;
  90 + final int horiBearingY;
  91 + final int horiAdvance;
  92 + final int vertBearingX;
  93 + final int vertBearingY;
  94 + final int vertAdvance;
  95 + final int ascent;
  96 + final int descent;
  97 +
  98 + PdfFontMetrics get metrics {
  99 + final coef = 1.0 / height;
  100 + return PdfFontMetrics(
  101 + bottom: horiBearingY * coef,
  102 + left: horiBearingX * coef,
  103 + top: horiBearingY * coef - height * coef,
  104 + right: horiAdvance * coef,
  105 + ascent: ascent * coef,
  106 + descent: horiBearingY * coef,
  107 + advanceWidth: horiAdvance * coef,
  108 + leftBearing: horiBearingX * coef,
  109 + );
  110 + }
  111 +
  112 + @override
  113 + String toString() =>
  114 + 'Bitmap Glyph ${width}x$height horiBearingX:$horiBearingX horiBearingY:$horiBearingY horiAdvance:$horiAdvance ascender:$ascent descender:$descent';
  115 +}
  116 +
71 class TtfParser { 117 class TtfParser {
72 TtfParser(ByteData bytes) : bytes = UnmodifiableByteDataView(bytes) { 118 TtfParser(ByteData bytes) : bytes = UnmodifiableByteDataView(bytes) {
73 final numTables = bytes.getUint16(4); 119 final numTables = bytes.getUint16(4);
@@ -92,14 +138,17 @@ class TtfParser { @@ -92,14 +138,17 @@ class TtfParser {
92 'Unable to find the `cmap` table. This file is not a supported TTF font'); 138 'Unable to find the `cmap` table. This file is not a supported TTF font');
93 assert(tableOffsets.containsKey(maxp_table), 139 assert(tableOffsets.containsKey(maxp_table),
94 'Unable to find the `maxp` table. This file is not a supported TTF font'); 140 'Unable to find the `maxp` table. This file is not a supported TTF font');
95 - assert(tableOffsets.containsKey(loca_table),  
96 - 'Unable to find the `loca` table. This file is not a supported TTF font');  
97 - assert(tableOffsets.containsKey(glyf_table),  
98 - 'Unable to find the `glyf` table. This file is not a supported TTF font');  
99 141
100 _parseCMap(); 142 _parseCMap();
101 - _parseIndexes();  
102 - _parseGlyphs(); 143 + if (tableOffsets.containsKey(loca_table) &&
  144 + tableOffsets.containsKey(glyf_table)) {
  145 + _parseIndexes();
  146 + _parseGlyphs();
  147 + }
  148 + if (tableOffsets.containsKey(cblc_table) &&
  149 + tableOffsets.containsKey(cbdt_table)) {
  150 + _parseBitmaps();
  151 + }
103 } 152 }
104 153
105 static const String head_table = 'head'; 154 static const String head_table = 'head';
@@ -110,14 +159,17 @@ class TtfParser { @@ -110,14 +159,17 @@ class TtfParser {
110 static const String maxp_table = 'maxp'; 159 static const String maxp_table = 'maxp';
111 static const String loca_table = 'loca'; 160 static const String loca_table = 'loca';
112 static const String glyf_table = 'glyf'; 161 static const String glyf_table = 'glyf';
  162 + static const String cblc_table = 'CBLC';
  163 + static const String cbdt_table = 'CBDT';
113 164
114 final UnmodifiableByteDataView bytes; 165 final UnmodifiableByteDataView bytes;
115 - final Map<String, int> tableOffsets = <String, int>{};  
116 - final Map<String, int> tableSize = <String, int>{}; 166 + final tableOffsets = <String, int>{};
  167 + final tableSize = <String, int>{};
117 168
118 - final Map<int, int> charToGlyphIndexMap = <int, int>{};  
119 - final List<int> glyphOffsets = <int>[];  
120 - final Map<int, PdfFontMetrics> glyphInfoMap = <int, PdfFontMetrics>{}; 169 + final charToGlyphIndexMap = <int, int>{};
  170 + final glyphOffsets = <int>[];
  171 + final glyphInfoMap = <int, PdfFontMetrics>{};
  172 + final bitmapOffsets = <int, TtfBitmapInfo>{};
121 173
122 int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18); 174 int get unitsPerEm => bytes.getUint16(tableOffsets[head_table]! + 18);
123 175
@@ -145,9 +197,14 @@ class TtfParser { @@ -145,9 +197,14 @@ class TtfParser {
145 197
146 bool get unicode => bytes.getUint32(0) == 0x10000; 198 bool get unicode => bytes.getUint32(0) == 0x10000;
147 199
  200 + bool get isBitmap => bitmapOffsets.isNotEmpty && glyphOffsets.isEmpty;
  201 +
148 // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html 202 // https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6name.html
149 String? getNameID(TtfParserName fontNameID) { 203 String? getNameID(TtfParserName fontNameID) {
150 - final basePosition = tableOffsets[name_table]!; 204 + final basePosition = tableOffsets[name_table];
  205 + if (basePosition == null) {
  206 + return null;
  207 + }
151 // final format = bytes.getUint16(basePosition); 208 // final format = bytes.getUint16(basePosition);
152 final count = bytes.getUint16(basePosition + 2); 209 final count = bytes.getUint16(basePosition + 2);
153 final stringOffset = bytes.getUint16(basePosition + 4); 210 final stringOffset = bytes.getUint16(basePosition + 4);
@@ -288,15 +345,15 @@ class TtfParser { @@ -288,15 +345,15 @@ class TtfParser {
288 } 345 }
289 346
290 void _parseIndexes() { 347 void _parseIndexes() {
291 - final basePosition = tableOffsets[loca_table]; 348 + final basePosition = tableOffsets[loca_table]!;
292 final numGlyphs = this.numGlyphs; 349 final numGlyphs = this.numGlyphs;
293 if (indexToLocFormat == 0) { 350 if (indexToLocFormat == 0) {
294 for (var i = 0; i < numGlyphs; i++) { 351 for (var i = 0; i < numGlyphs; i++) {
295 - glyphOffsets.add(bytes.getUint16(basePosition! + i * 2) * 2); 352 + glyphOffsets.add(bytes.getUint16(basePosition + i * 2) * 2);
296 } 353 }
297 } else { 354 } else {
298 for (var i = 0; i < numGlyphs; i++) { 355 for (var i = 0; i < numGlyphs; i++) {
299 - glyphOffsets.add(bytes.getUint32(basePosition! + i * 4)); 356 + glyphOffsets.add(bytes.getUint32(basePosition + i * 4));
300 } 357 }
301 } 358 }
302 } 359 }
@@ -465,4 +522,92 @@ class TtfParser { @@ -465,4 +522,92 @@ class TtfParser {
465 } 522 }
466 return String.fromCharCodes(charCodes); 523 return String.fromCharCodes(charCodes);
467 } 524 }
  525 +
  526 + // https://docs.microsoft.com/en-us/typography/opentype/spec/ebdt
  527 + void _parseBitmaps() {
  528 + final baseOffset = tableOffsets[cblc_table]!;
  529 + final pngOffset = tableOffsets[cbdt_table]!;
  530 +
  531 + // CBLC Header
  532 + final numSizes = bytes.getUint32(baseOffset + 4);
  533 + var bitmapSize = baseOffset + 8;
  534 +
  535 + for (var bitmapSizeIndex = 0;
  536 + bitmapSizeIndex < numSizes;
  537 + bitmapSizeIndex++) {
  538 + // BitmapSize Record
  539 + final indexSubTableArrayOffset = baseOffset + bytes.getUint32(bitmapSize);
  540 + // final indexTablesSize = bytes.getUint32(bitmapSize + 4);
  541 + final numberOfIndexSubTables = bytes.getUint32(bitmapSize + 8);
  542 +
  543 + final ascender = bytes.getInt8(bitmapSize + 12);
  544 + final descender = bytes.getInt8(bitmapSize + 13);
  545 +
  546 + // final startGlyphIndex = bytes.getUint16(bitmapSize + 16 + 12 * 2);
  547 + // final endGlyphIndex = bytes.getUint16(bitmapSize + 16 + 12 * 2 + 2);
  548 + // final ppemX = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 4);
  549 + // final ppemY = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 5);
  550 + // final bitDepth = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 6);
  551 + // final flags = bytes.getUint8(bitmapSize + 16 + 12 * 2 + 7);
  552 +
  553 + var subTableArrayOffset = indexSubTableArrayOffset;
  554 + for (var indexSubTable = 0;
  555 + indexSubTable < numberOfIndexSubTables;
  556 + indexSubTable++) {
  557 + // IndexSubTableArray
  558 + final firstGlyphIndex = bytes.getUint16(subTableArrayOffset);
  559 + final lastGlyphIndex = bytes.getUint16(subTableArrayOffset + 2);
  560 + final additionalOffsetToIndexSubtable =
  561 + indexSubTableArrayOffset + bytes.getUint32(subTableArrayOffset + 4);
  562 +
  563 + // IndexSubHeader
  564 + final indexFormat = bytes.getUint16(additionalOffsetToIndexSubtable);
  565 + final imageFormat =
  566 + bytes.getUint16(additionalOffsetToIndexSubtable + 2);
  567 + final imageDataOffset =
  568 + pngOffset + bytes.getUint32(additionalOffsetToIndexSubtable + 4);
  569 +
  570 + if (indexFormat == 1) {
  571 + // IndexSubTable1
  572 +
  573 + for (var glyph = firstGlyphIndex; glyph <= lastGlyphIndex; glyph++) {
  574 + final sbitOffset = imageDataOffset +
  575 + bytes.getUint32(additionalOffsetToIndexSubtable +
  576 + (glyph - firstGlyphIndex + 2) * 4);
  577 +
  578 + if (imageFormat == 17) {
  579 + final height = bytes.getUint8(sbitOffset);
  580 + final width = bytes.getUint8(sbitOffset + 1);
  581 + final bearingX = bytes.getInt8(sbitOffset + 2);
  582 + final bearingY = bytes.getInt8(sbitOffset + 3);
  583 + final advance = bytes.getUint8(sbitOffset + 4);
  584 + final dataLen = bytes.getUint32(sbitOffset + 5);
  585 +
  586 + bitmapOffsets[glyph] = TtfBitmapInfo(
  587 + bytes.buffer.asUint8List(
  588 + bytes.offsetInBytes + sbitOffset + 9,
  589 + dataLen,
  590 + ),
  591 + height,
  592 + width,
  593 + bearingX,
  594 + bearingY,
  595 + advance,
  596 + 0,
  597 + 0,
  598 + 0,
  599 + ascender,
  600 + descender);
  601 + }
  602 + }
  603 + }
  604 +
  605 + subTableArrayOffset += 8;
  606 + }
  607 + bitmapSize += 16 + 12 * 2 + 8;
  608 + }
  609 + }
  610 +
  611 + TtfBitmapInfo? getBitmap(int charcode) =>
  612 + bitmapOffsets[charToGlyphIndexMap[charcode]];
468 } 613 }
@@ -23,7 +23,7 @@ class PdfUnicodeCmap extends PdfObjectStream { @@ -23,7 +23,7 @@ class PdfUnicodeCmap extends PdfObjectStream {
23 PdfUnicodeCmap(PdfDocument pdfDocument, this.protect) : super(pdfDocument); 23 PdfUnicodeCmap(PdfDocument pdfDocument, this.protect) : super(pdfDocument);
24 24
25 /// List of characters 25 /// List of characters
26 - final List<int> cmap = <int>[0]; 26 + final cmap = <int>[];
27 27
28 /// Protects the text from being "seen" by the PDF reader. 28 /// Protects the text from being "seen" by the PDF reader.
29 final bool protect; 29 final bool protect;
@@ -24,6 +24,8 @@ import 'annotations.dart'; @@ -24,6 +24,8 @@ import 'annotations.dart';
24 import 'basic.dart'; 24 import 'basic.dart';
25 import 'document.dart'; 25 import 'document.dart';
26 import 'geometry.dart'; 26 import 'geometry.dart';
  27 +import 'image.dart';
  28 +import 'image_provider.dart';
27 import 'multi_page.dart'; 29 import 'multi_page.dart';
28 import 'placeholders.dart'; 30 import 'placeholders.dart';
29 import 'text_style.dart'; 31 import 'text_style.dart';
@@ -690,6 +692,25 @@ class RichText extends Widget with SpanningWidget { @@ -690,6 +692,25 @@ class RichText extends Widget with SpanningWidget {
690 _decorations.add(td); 692 _decorations.add(td);
691 } 693 }
692 694
  695 + InlineSpan _addEmoji({
  696 + required TtfBitmapInfo bitmap,
  697 + double baseline = 0,
  698 + required TextStyle style,
  699 + AnnotationBuilder? annotation,
  700 + }) {
  701 + final metrics = bitmap.metrics * style.fontSize!;
  702 +
  703 + return WidgetSpan(
  704 + child: SizedBox(
  705 + height: style.fontSize,
  706 + child: Image(MemoryImage(bitmap.data)),
  707 + ),
  708 + style: style,
  709 + baseline: baseline + metrics.ascent + metrics.descent - metrics.height,
  710 + annotation: annotation,
  711 + );
  712 + }
  713 +
693 InlineSpan _addText({ 714 InlineSpan _addText({
694 required List<int> text, 715 required List<int> text,
695 int start = 0, 716 int start = 0,
@@ -770,6 +791,19 @@ class RichText extends Widget with SpanningWidget { @@ -770,6 +791,19 @@ class RichText extends Widget with SpanningWidget {
770 for (final fb in style.fontFallback) { 791 for (final fb in style.fontFallback) {
771 final font = fb.getFont(context); 792 final font = fb.getFont(context);
772 if (font.isRuneSupported(rune)) { 793 if (font.isRuneSupported(rune)) {
  794 + if (font is PdfTtfFont) {
  795 + final bitmap = font.font.getBitmap(rune);
  796 + if (bitmap != null) {
  797 + spans.add(_addEmoji(
  798 + bitmap: bitmap,
  799 + style: style,
  800 + baseline: span.baseline,
  801 + annotation: annotation,
  802 + ));
  803 + found = true;
  804 + break;
  805 + }
  806 + }
773 spans.add(_addText( 807 spans.add(_addText(
774 text: [rune], 808 text: [rune],
775 style: style.copyWith( 809 style: style.copyWith(
@@ -27,6 +27,7 @@ late Document pdf; @@ -27,6 +27,7 @@ late Document pdf;
27 late Font ttf; 27 late Font ttf;
28 late Font ttfBold; 28 late Font ttfBold;
29 late Font asian; 29 late Font asian;
  30 +late Font emoji;
30 31
31 Iterable<TextDecoration> permute( 32 Iterable<TextDecoration> permute(
32 List<TextDecoration> prefix, List<TextDecoration> remaining) sync* { 33 List<TextDecoration> prefix, List<TextDecoration> remaining) sync* {
@@ -48,6 +49,7 @@ void main() { @@ -48,6 +49,7 @@ void main() {
48 ttf = loadFont('open-sans.ttf'); 49 ttf = loadFont('open-sans.ttf');
49 ttfBold = loadFont('open-sans-bold.ttf'); 50 ttfBold = loadFont('open-sans-bold.ttf');
50 asian = loadFont('genyomintw.ttf'); 51 asian = loadFont('genyomintw.ttf');
  52 + emoji = loadFont('emoji.ttf');
51 pdf = Document(); 53 pdf = Document();
52 }); 54 });
53 55
@@ -357,6 +359,20 @@ void main() { @@ -357,6 +359,20 @@ void main() {
357 ); 359 );
358 }); 360 });
359 361
  362 + test('Text Widgets Emojis', () {
  363 + pdf.addPage(
  364 + Page(
  365 + build: (Context context) => Text(
  366 + 'Hello 🐈! Dancing 💃🏃',
  367 + style: TextStyle(
  368 + fontSize: 30,
  369 + fontFallback: [emoji],
  370 + ),
  371 + ),
  372 + ),
  373 + );
  374 + });
  375 +
360 tearDownAll(() async { 376 tearDownAll(() async {
361 final file = File('widgets-text.pdf'); 377 final file = File('widgets-text.pdf');
362 await file.writeAsBytes(await pdf.save()); 378 await file.writeAsBytes(await pdf.save());
@@ -128,8 +128,12 @@ void main(List<String> args) async { @@ -128,8 +128,12 @@ void main(List<String> args) async {
128 } 128 }
129 129
130 for (final entry in <String, String>{ 130 for (final entry in <String, String>{
  131 + 'CupertinoIcons':
  132 + 'https://github.com/flutter/packages/blob/master/third_party/packages/cupertino_icons/assets/CupertinoIcons.ttf',
131 'MaterialIcons': 133 'MaterialIcons':
132 'https://fonts.gstatic.com/s/materialicons/v98/flUhRq6tzZclQEJ-Vdg-IuiaDsNZ.ttf', 134 'https://fonts.gstatic.com/s/materialicons/v98/flUhRq6tzZclQEJ-Vdg-IuiaDsNZ.ttf',
  135 + 'NotoColorEmoji':
  136 + 'https://github.com/googlefonts/noto-emoji/raw/main/fonts/NotoColorEmoji.ttf',
133 }.entries) { 137 }.entries) {
134 output.writeln(''); 138 output.writeln('');
135 output.writeln('/// ${entry.key}'); 139 output.writeln('/// ${entry.key}');