From 6880d95f7d4fcdad31073023bc4ed03e1be4c4f8 Mon Sep 17 00:00:00 2001 From: Tarek Tolba <98803924+TarekTolba1@users.noreply.github.com> Date: Tue, 9 Jul 2024 11:55:01 +0300 Subject: [PATCH] Rich text (#358) Co-authored-by: Luiz Guilherme Alvarez --- lib/src/parser/parse.dart | 2 +- lib/src/save/save_file.dart | 6 +- lib/src/sharedStrings/shared_strings.dart | 115 ++++++++++++++++++++++ lib/src/sheet/data_model.dart | 7 +- lib/src/sheet/sheet.dart | 3 +- test/excel_test.dart | 30 ++++++ test/test_resources/richText.xlsx | Bin 0 -> 5365 bytes 7 files changed, 155 insertions(+), 8 deletions(-) create mode 100644 test/test_resources/richText.xlsx diff --git a/lib/src/parser/parse.dart b/lib/src/parser/parse.dart index 6eee9510..29682976 100644 --- a/lib/src/parser/parse.dart +++ b/lib/src/parser/parse.dart @@ -610,7 +610,7 @@ class Parser { case 's': final sharedString = _excel._sharedStrings .value(int.parse(_parseValue(node.findElements('v').first))); - value = TextCellValue(sharedString!.stringValue); + value = TextCellValue.span(sharedString!.textSpan); break; // boolean case 'b': diff --git a/lib/src/save/save_file.dart b/lib/src/save/save_file.dart index b90543c4..ffb56710 100644 --- a/lib/src/save/save_file.dart +++ b/lib/src/save/save_file.dart @@ -46,11 +46,11 @@ class Save { CellValue? value, NumFormat? numberFormat) { SharedString? sharedString; if (value is TextCellValue) { - sharedString = _excel._sharedStrings.tryFind(value.value); + sharedString = _excel._sharedStrings.tryFind(value.toString()); if (sharedString != null) { - _excel._sharedStrings.add(sharedString, value.value); + _excel._sharedStrings.add(sharedString, value.toString()); } else { - sharedString = _excel._sharedStrings.addFromString(value.value); + sharedString = _excel._sharedStrings.addFromString(value.toString()); } } diff --git a/lib/src/sharedStrings/shared_strings.dart b/lib/src/sharedStrings/shared_strings.dart index 453266de..13a23133 100644 --- a/lib/src/sharedStrings/shared_strings.dart +++ b/lib/src/sharedStrings/shared_strings.dart @@ -77,6 +77,91 @@ class SharedString { return stringValue; } + TextSpan get textSpan { + bool getBool(XmlElement element) { + return bool.tryParse(element.getAttribute('val') ?? '') ?? true; + } + + int getDouble(XmlElement element) { + // Should be double + return double.parse(element.getAttribute('val')!).toInt(); + } + + String? text; + List? children; + + /// SharedStringItem + /// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.sharedstringitem?view=openxml-3.0.1 + assert(node.localName == 'si'); //18.4.8 si (String Item) + + for (final child in node.childElements) { + switch (child.localName) { + /// Text + /// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.text?view=openxml-3.0.1 + case 't': //18.4.12 t (Text) + text = (text ?? '') + child.innerText; + break; + + /// Rich Text Run + /// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.run?view=openxml-3.0.1 + case 'r': //18.4.4 r (Rich Text Run) + var style = CellStyle(); + for (final runChild in child.childElements) { + switch (runChild.localName) { + /// RunProperties + /// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.runproperties?view=openxml-3.0.1 + case 'rPr': + for (final runProperty in runChild.childElements) { + switch (runProperty.localName) { + case 'b': //18.8.2 b (Bold) + style = style.copyWith(boldVal: getBool(runProperty)); + break; + case 'i': //18.8.26 i (Italic) + style = style.copyWith(italicVal: getBool(runProperty)); + break; + case 'u': //18.4.13 u (Underline) + style = style.copyWith( + underlineVal: + runProperty.getAttribute('val') == 'double' + ? Underline.Double + : Underline.Single); + break; + case 'sz': //18.4.11 sz (Font Size) + style = + style.copyWith(fontSizeVal: getDouble(runProperty)); + break; + case 'rFont': //18.4.5 rFont (Font) + style = style.copyWith( + fontFamilyVal: runProperty.getAttribute('val')); + break; + case 'color': //18.3.1.15 color (Data Bar Color) + style = style.copyWith( + fontColorHexVal: + runProperty.getAttribute('rgb')?.excelColor); + break; + } + } + break; + + /// Text + case 't': //18.4.12 t (Text) + if (children == null) children = []; + children.add(TextSpan(text: runChild.innerText, style: style)); + break; + } + } + break; + + /// Phonetic Run + /// https://learn.microsoft.com/en-us/dotnet/api/documentformat.openxml.spreadsheet.phoneticrun?view=openxml-3.0.1 + case 'rPh': //18.4.6 rPh (Phonetic Run) + break; + } + } + + return TextSpan(text: text, children: children); + } + String get stringValue { var buffer = StringBuffer(); node.findAllElements('t').forEach((child) { @@ -102,3 +187,33 @@ class SharedString { return value.isNotEmpty && value == stringValue; } } + +class TextSpan { + final String? text; + final List? children; + final CellStyle? style; + + const TextSpan({this.children, this.text, this.style}); + + @override + String toString() { + String r = ''; + if (text != null) r += text!; + if (children != null) r += children!.join(); + return r; + } + + @override + operator ==(Object other) { + if (identical(this, other)) return true; + if (other.runtimeType != runtimeType) return false; + return other is TextSpan && + other.text == text && + other.style == style && + ListEquality().equals(other.children, children); + } + + @override + int get hashCode => + Object.hash(text, style, Object.hashAll(children ?? const [])); +} diff --git a/lib/src/sheet/data_model.dart b/lib/src/sheet/data_model.dart index 9d7874a1..5ac374cf 100644 --- a/lib/src/sheet/data_model.dart +++ b/lib/src/sheet/data_model.dart @@ -211,13 +211,14 @@ class DateCellValue extends CellValue { } class TextCellValue extends CellValue { - final String value; + final TextSpan value; - const TextCellValue(this.value); + TextCellValue(String text) : value = TextSpan(text: text); + TextCellValue.span(this.value); @override String toString() { - return value; + return value.toString(); } @override diff --git a/lib/src/sheet/sheet.dart b/lib/src/sheet/sheet.dart index cf0249cc..c1a6906f 100644 --- a/lib/src/sheet/sheet.dart +++ b/lib/src/sheet/sheet.dart @@ -1287,7 +1287,8 @@ class Sheet { if (sourceData is! TextCellValue) { continue; } - final result = sourceData.value.replaceAllMapped(source, (match) { + final result = + sourceData.value.toString().replaceAllMapped(source, (match) { if (first == -1 || first != replaceCount) { ++replaceCount; return match.input.replaceRange(match.start, match.end, target); diff --git a/test/excel_test.dart b/test/excel_test.dart index 1e0a27a7..8292b063 100644 --- a/test/excel_test.dart +++ b/test/excel_test.dart @@ -823,6 +823,36 @@ void main() { }); }); + group('Cell Style', () { + test('read file with rich text', () { + final file = './test/test_resources/richText.xlsx'; + final bytes = File(file).readAsBytesSync(); + final excel = Excel.decodeBytes(bytes); + final Sheet sheetObject = excel.tables['Sheet1']!; + final redHex = 'FFFF0000'; + final blueHex = 'FF2A6099'; + + final cellA1 = sheetObject.cell(CellIndex.indexByString('A1')).value + as TextCellValue; + expect(cellA1.value.children![0].style!.fontSize, 12); + expect(cellA1.value.children![0].style!.fontColor.colorHex, redHex); + expect(cellA1.value.children![1].style!.fontSize, 10); + expect(cellA1.value.children![1].style!.fontColor.colorHex, blueHex); + + final cellA2 = sheetObject.cell(CellIndex.indexByString('A2')).value + as TextCellValue; + expect(cellA2.value.children![0].style!.isBold, true); + expect(cellA2.value.children![0].style!.isItalic, false); + expect(cellA2.value.children![1].style!.isBold, false); + expect(cellA2.value.children![1].style!.isItalic, true); + + final cellA3 = sheetObject.cell(CellIndex.indexByString('A3')).value + as TextCellValue; + expect(cellA3.value.children![0].style!.fontFamily, "Skia"); + expect(cellA3.value.children![1].style!.fontFamily, "Arial"); + }); + }); + group('rPh tag', () { test('Read Cell shared text without rPh elements', () { var file = './test/test_resources/rphSample.xlsx'; diff --git a/test/test_resources/richText.xlsx b/test/test_resources/richText.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..315649650b47144d459850038044b998245c4ce6 GIT binary patch literal 5365 zcmaJ_2|Sd2*S0gpZbbIAY-6V(5;C$c*~vOW_92sP?9tdKNg;c(Oh`latRZV6jqK#X zAWN2PA3g8)=IeXk=iI+p?%(e||NFlG=bY=h&N0v?CIJypP*4zvUR^RLIA_4q|F&=^ zcVBU_vu_M~K#N$CE@nLhl^leqX1W2ZYK$GsxET_b-g|6+<*CHQdm%xQ6qdz>jSaeSH0!Rr4KG6?}jeBC=(cHlT+L&iKv-AeeJ761gGc!ZDVLpueg7D4==cz zgO`_^*nJQ8gj?O7;}Ud@yCNi$9$=fhY-z-1iOB>LbVc1y(a;NSC{Wi%THeVdy;GqO zPK{Ph9yrzTG{AB)(`|{P#XQ%fx7GfgqCu@#6s`k5?x>J1Ilya1`ovPDSIq89-}1y} zLEp9E!1bXZCr48rIp9=H0I6(^+C*61&0^iY6rHLq563X(YNB_RKMm=0j-CNK6r!T{ zxu|ZZ@x~vqnGyMJUd?1mnH zj0#lkZu^tXdc*&C1as*UMuMk<4mu*^5>6tb|LNjEmNu8y7D} zYO+_1R`R|7+H&+0=R*Nl`7%7D^!PD_>chpYD_a4baw(e1coELv7bW&aqJ#5bC?Qj= zX!@AwU{TCShCC&!?hTc$H!57Ovp+()AdOQ-CB$kYlqD1z_Gu5YB1OT4kbTW$X_)Jl?4HhBp9l|xitsWHfs&^cC}vTH%za|r1REw? z%*w~AWR?QiXWa_xQ?$fPa^zM>EuR82R$};FLC%59bNm6h5I1rcZ2pCy4TmZ&JQw`_ zNmp#mvz+FgJ5LDb8f?jK%~M%eGGdAuM20Eq7Gu;rM$r;+-hD+x_3Q$DLqx zbMAb?fukGw=x5*RCd{pT)6kCZ?L?Rk?plu%dDbQLuC+Z|7@hBoBhuadsbBnX%Rwf@ zumCC1dF`PN$ipsD)~cg@F4DS^Q_i&{=px~heLip^Z%vuRCzSzj+viS{*_$%8GWuEl zTQ{it+H^RIT2|<#8y^z3TQ^jDI{fb;MlT)rYUv4Fu2xvKs?Z6WLiUaK4F*C}k8zVr8m<{3wm6$t)8z_o8(=qPmYkeuUstC1HqY-cVK&;!JT6cP#=j?dL8w zaQWOB@PSr-YB3}qVLPO zQQYmXpe*4ab80#UZ1b&|FUK65yFb2fKY{ItpOrh!)1=-vr=|Xx{h!L6_3v{3n?BDI z#ott#qU+WsLG@t$hzYvep-Ju-4{3~7DL>jH>@=JmY?niiVo#7_Y*v&`_`yempSPNB z{#cetSdML1qarKGBZ+S5@TX`DKU!OzxoFy#H#zMhkW|6NP};ya7RIb8ZdtVju@G}; zO}=`&pj%6?_;Yt%9X@vL(8x0ex`J z6zL(OB!_7caVhd`kNpLu1!Z22F}|Mi$ZR7y1YpGW4(ECx&<<4%L{4`j-#^$Q*y8dch9 zr@n|7Xr1AvA#U{z7nfk_-*O5-l7540D6B#qrxK=8-q3kEU{07u3B&w=sO0`UN zxi*c9&|KBqt>@*JtBLk$3g5|@K&cn=nkcCPgy@DzzVYLdnIxwQGOp;)5>l=Dr4|}i z$!klJ0z7cU>L`o*(hW@)zc6{WOr_%e)|Cbe$G~B>eaW6PnH&1*8ypKHWzdm?4MY65 zh!8@aY1og;$2>XaZU%n0h}y0irPF-O`GJx^vqc;nq+#u+Vc7D~hHg|+tU zC2RuMqt`(h4EL$Yhwn>0x3SSt$l?~UjU8cs@rdv(GTQx38Z>k!x{1%K)vl56Q%yKI zm+dxVxG1S1_5iyn#gEmsBYyT77h7Wg?Uu1oJV1G6biZs6SP^x3VXe4t@9qsfA1pph zICgg6({TU9m&Q&X7wGyJ1+Pkdl}jpMY~Z3AgQ}M|9(M#9AFYi6)NM(x#P;QCGmKm< z7+j|A)4$9+rO?ES&7DrKea22JW_@IEZPTBkus1|l2OYOYYitE1mdeTyy3Ge60I8-= zLY8r+;9N_sJkxEaS1n8wpG$jRWnmXE7yW;TQi--3$t#=V_ikzKG2}(-2vN4w=|@=z zMoUo|zHD=Qu(`;V*!<*^ABR*JZG_F0o?PJpb*E<8Hy(nS=FM7c<78_qcXbb`%VaIs zGm>ukhzMm8vE9swj1*u-GaxY6f#svEt{^*0Q1WF@_T%t7ZZ-~w?{~aStBmdN2cA2G zCY!@wkC1zYz^x^KLKDqeZiX_7a$}0Cn+#XG`N(rZ!Y3(1M&X_L4gGJ;JeImdyT{XZ zY-KXb?*VOVaZ(4rbd=X6UhXS7h;Zt4R4No(*-MH2Q!GTGR^-J556>W-YJ^7Jm`9!{i!2O22rI9K*mXBr!Nv_L(3{20n+-He|Xy*5JWK zVsP$C3NymIlHcj+cgYQM?f{Zhu_Cw}8}KhJL>I$8fIm;iw$PF`t6@b#?kWjKbl38- zIron#mMffZg!Nb8=;>q476idQdDz%uO^j6M^Z_)+H}1Q9+?6xPzOFz2Na|F(;{H*) z(x2`*%*)XL?&a+(?&t+~I>YJ^quctC5}=S>v*acT#d39cDtKaU6lH-dgi1Ad&~osx{CTbE$F7eE)RC==x20B=7ARhoIo74KPCnDNS2!3jU=s3usrqRozR#f?dcF&T&r3H4cT$I~(S`fi- z2CfA|++TWH0rm=JiZm8ndd;Ce>jnT2pD!U=+NaLQu=;{)MrGSrQCi!1%s8w6xnw_&$O&-fI@y^1&)Js=Mr)BlYm1vGmH^?YlW>mNY z_^XwoxHPjtl=GvTR7T?Kj?oDH3bTXEPdsC6oxz5Yv(H(9tRjUzDDtm$C<`0W08%_$ zvB@ENh|uq*+C29#o<^(bt}48_WiCssRTEyn5@#eoGb;Cbn(%|Dsm0}fmARlWH*n$$ z?dD7QvF<^^U`#UvV|VW!KevhBg@Jnd^$T@89)yzmEH#ga(DI0x6cKm3@*zMz@xhW2 zP6prhb|FJj8^;wC^GrzFB21z~izg~@B@AdwOfoX)Ux?BUflkGE5F=fFq;1DW_KOpU&r1|5+>l+jLK%^VIowWB;e?d0Bkc7W_6v@?Wn1rAhd6 zo%8&7CZd0vDaF5^|NlwqKUX=A_-9=D+a6Q>{UU!d>z^x}N1`)k_-z21|G*P}x}Pt* yGr0O~Ri{z;H_iMDy#Dk)pBZPR`P)cOpXEOks)05s+1W*a(?j_bJ98M$&i)6`%neQe literal 0 HcmV?d00001