From cb71e72a2a4223911732b7430ab5072fe12c2053 Mon Sep 17 00:00:00 2001 From: Xamora Date: Wed, 9 Jul 2025 14:36:11 +0200 Subject: [PATCH] starting project, replace text with text or image --- README.md | 4 +- bin/main.dart | 13 --- lib/docx.dart | 156 ++++++++++++++++++++------------ pubspec.yaml | 1 + test/assets/docx_test.docx | Bin 5796 -> 0 bytes test/assets/docx_test_copy.docx | Bin 5784 -> 5790 bytes test/docx_test.dart | 13 ++- 7 files changed, 110 insertions(+), 77 deletions(-) delete mode 100644 bin/main.dart delete mode 100644 test/assets/docx_test.docx diff --git a/README.md b/README.md index b504558..6eb097e 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ # Docx Editor Library -IN WIP \ No newline at end of file +IN WIP + +0.0.1 diff --git a/bin/main.dart b/bin/main.dart deleted file mode 100644 index 75a2699..0000000 --- a/bin/main.dart +++ /dev/null @@ -1,13 +0,0 @@ -import 'package:docx/docx.dart'; -import 'dart:io'; -import 'package:docx/replace.dart'; - -void main(List args) { - DocxEditor docx = DocxEditor(File('test/assets/docx_test.docx'), { - 'Title': ReplaceContent(value: 'NewTitle'), - 'A': ReplaceContent(value: 'AModified'), - 'ImageCat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), - 'ImageDog': ReplaceContent(img: DocxImage(file: File('test/assets/dog.png'), sizeX: 400, sizeY: 300)), - 'Footer': ReplaceContent(img: DocxImage(file: File('test/assets/dog.jpg'), sizeX: 400, sizeY: 300)) - }); -} diff --git a/lib/docx.dart b/lib/docx.dart index 8811d54..6935221 100644 --- a/lib/docx.dart +++ b/lib/docx.dart @@ -1,3 +1,4 @@ +import 'dart:collection'; import 'dart:convert'; import 'dart:io'; @@ -8,13 +9,19 @@ import 'package:docx/replace.dart'; class DocxEditor { - File fileDocx; + File fileInDocx; + String? pathOutDocx; - /// [fileDocx] The Docx file... - /// [replaceMap] All text and image to change - DocxEditor(this.fileDocx, Map replaceMap) { - if (!fileDocx.existsSync()) - throw(PathNotFoundException(fileDocx.path, OSError('File not found', 404))); + /// [fileInDocx] The Docx file (required) + /// [pathOutDocx] The path where new file is save (optional) + /// [replaceMap] All text and image to change (required) + DocxEditor({ + required this.fileInDocx, + this.pathOutDocx, + required Map replaceMap + }) { + if (!fileInDocx.existsSync()) + throw(PathNotFoundException(fileInDocx.path, OSError('File not found', 404))); List documentsXml = editById(replaceMap); save(documentsXml, replaceMap); @@ -22,67 +29,74 @@ class DocxEditor { /// This function search ID in [replaceMap] and replace by his value or by a image List editById(Map replaceMap) { - Uint8List content = fileDocx.readAsBytesSync(); + Uint8List content = fileInDocx.readAsBytesSync(); - Archive archive = ZipDecoder().decodeBytes(content); - ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/document.xml'); - XmlDocument documentXml = XmlDocument.parse(utf8.decode(archiveFile.content as List)); + Archive archive = ZipDecoder().decodeBytes(content); + ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/document.xml'); + XmlDocument documentXml = XmlDocument.parse(utf8.decode(archiveFile.content as List)); - XmlDocument documentXmlRels = addImageinArchive(archive, replaceMap); + XmlDocument documentXmlRels = addImageinArchive(archive, replaceMap); - print(documentXml.toXmlString(pretty: true, indent: ' ')); + //print(documentXml.toXmlString(pretty: true, indent: ' ')); - // TODO: Rework this function, doesn't work with ambiguous ID to search, or multiple Id in one paragraphe, separator like '_' or '-' or space - // TODO: Essayer de parcourir par rapport au paragraphe () et non les runs () - for (XmlElement node in documentXml.findAllElements('w:r')) { - String ndText = node.innerText.toLowerCase(); - //Iterable replacesId = replaceMap.keys.where((rm) => rm.toLowerCase() == ndText); - for (String key in replaceMap.keys) { - Iterable replacesId = RegExp(key.toLowerCase()).allMatches(ndText).map((m) => m.group(0)!); - XmlNode? parent; - for (int i = 0; i < replacesId.length; i++) { - ReplaceContent replaceCnt = replaceMap[key]!; - if (replaceCnt.img != null) - parent = putImage(node, replaceCnt, parent); - else { /// Replace Text by other text - int start = node.innerXml.indexOf(''); - int end = node.innerXml.indexOf(''); - node.innerXml = node.innerXml.replaceRange(start + 5, end, replaceCnt.value!); + // sort descending orderz + final keys = replaceMap.keys.toList()..sort((a, b) => b.length.compareTo(a.length)); - } - } - } + final pattern = RegExp( + keys.map((key) => '(?<=^|\\s)${RegExp.escape(key)}(?=\\s|\$)').join('|'), + ); + + // replace image + for (XmlElement node in documentXml.findAllElements('w:t')) { + String ndText = node.innerText.toLowerCase(); + if (ndText.isEmpty) + continue; + XmlElement? parent; + for (String key in replaceMap.keys) { + ReplaceContent replaceCnt = replaceMap[key]!; + if (replaceCnt.img == null) + continue; + for (RegExpMatch regMatch in RegExp('(?<=^|\\s)${key.toLowerCase()}(?=\\s|\$)').allMatches(ndText)) + parent = putImage(node.parentElement, replaceCnt, parent); } + } + + // Only replace text! + for (XmlElement paragraph in documentXml.findAllElements('w:p')) { + List texts = paragraph.findAllElements('w:t').toList(); + if (texts.isEmpty) + continue; + + String original = texts.map((t) { + return t.innerText; + }).join(); + + String replaced = original.replaceAllMapped(pattern, (match) { + ReplaceContent rc = replaceMap[match[0]]!; + return rc.img == null ? rc.value! : ''; + }); + + if (original == replaced) + continue; + + final firstRun = paragraph.findElements('w:r').first; + final newRun = firstRun.copy(); + newRun.findAllElements('w:t').first.innerText = replaced; // Update with new text + + // replace the first w:r with the new + firstRun.replace(newRun); + } return [documentXml, documentXmlRels]; } - /// Add all image in [replaceMap] to Archive in 'word/_rels/document.xml.rels' - XmlDocument addImageinArchive(Archive archive, Map replaceMap) { - - ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels'); - XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List)); - String documentXmlStr = documentXmlRels.toXmlString(); - RegExp exp = RegExp(r'Id="rId([0-9]+)"'); - Iterable matches = exp.allMatches(documentXmlStr); - int maxId = matches.isEmpty ? 1 : int.parse(matches.last.group(1)!); - - for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) { - replaceCnt.img!.id = ++maxId; - String text = '\n'; - int lastIndex = documentXmlStr.lastIndexOf('/>') + 3; - documentXmlStr = documentXmlStr.replaceRange(lastIndex, lastIndex, text); - } - documentXmlRels = XmlDocument.parse(documentXmlStr); - return documentXmlRels; - } - + // TODO: Find a solution to put multiple image in only one /// Replaces the text ([node]) with an image ([replaceContent]) - XmlNode? putImage(XmlElement node, ReplaceContent replaceContent, XmlNode? newParent) { + XmlElement? putImage(XmlElement? node, ReplaceContent replaceContent, XmlElement? newParent) { - if (node.parent == null && newParent != null) + if (node!.parentElement == null && newParent != null) node.attachParent(newParent); - XmlNode? parent = node.parent; + XmlElement? parent = node.parentElement; String nodeStr = node.toXmlString(); node.replace(XmlDocumentFragment.parse(""" $nodeStr @@ -124,12 +138,32 @@ class DocxEditor { return parent; } - /// Recreate a Archive to add modification and save it on [fileDocx] + /// Add all image in [replaceMap] to Archive in 'word/_rels/document.xml.rels' + XmlDocument addImageinArchive(Archive archive, Map replaceMap) { + + ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels'); + XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List)); + String documentXmlStr = documentXmlRels.toXmlString(); + RegExp exp = RegExp(r'Id="rId([0-9]+)"'); + Iterable matches = exp.allMatches(documentXmlStr); + int maxId = matches.isEmpty ? 1 : int.parse(matches.last.group(1)!); + + for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) { + replaceCnt.img!.id = ++maxId; + String text = '\n'; + int lastIndex = documentXmlStr.lastIndexOf('/>') + 3; + documentXmlStr = documentXmlStr.replaceRange(lastIndex, lastIndex, text); + } + documentXmlRels = XmlDocument.parse(documentXmlStr); + return documentXmlRels; + } + + /// Recreate a Archive to add modification and save it on [fileInDocx] /// [documentsXml] is list -> ['word/document.xml', 'word/_rels/document.xml.rels'] /// [replaceMap] for upload file image in docx file - void save(List documentsXml, Map replaceMap) { + File save(List documentsXml, Map replaceMap) { try { - final originalBytes = fileDocx.readAsBytesSync(); + final originalBytes = fileInDocx.readAsBytesSync(); final archive = ZipDecoder().decodeBytes(originalBytes); final Uint8List uListDocumentXml = utf8.encode(documentsXml[0].toXmlString()); @@ -167,9 +201,13 @@ class DocxEditor { } final zipData = ZipEncoder().encode(newArchive); - fileDocx.writeAsBytesSync(zipData); + + File newFile = pathOutDocx == null ? fileInDocx : File(pathOutDocx!); + newFile.writeAsBytesSync(zipData); + return newFile; + } catch (e) { throw StateError('DOCX BUILDER ERROR: $e'); } } -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 7649126..766cdc1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ environment: dependencies: xml: ^6.6.0 archive: ^4.0.7 + xml2json: ^6.2.7 # path: ^1.8.0 dev_dependencies: diff --git a/test/assets/docx_test.docx b/test/assets/docx_test.docx deleted file mode 100644 index 14791372340752c91c4fc877fbbd6f78e4203212..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 5796 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 diff --git a/test/assets/docx_test_copy.docx b/test/assets/docx_test_copy.docx index c676b5d573ab6fedf80181c6bc5838f38503f087..204434a8e35e12516405f3700384bfda45212b45 100644 GIT binary patch delta 1223 zcmbQCJ5QH4z?+#xgn@&DgCWNI{4{*Zx4?e}nroB*& zZYkH;(6@4q=+Y)ht0vdaU(_DX`gG~QW=&HG^#g48JR(nh<;k6UStR-Nc7>U;6|JA1 z>@t~T$$yqD`&j+{8*BwKuE{zFH_owa%RU_Nz3)ZJ%+L3N-uqmXcp~G>C^TjB3?2@< zY*~rS^>!IExR;8&Tk&w!7Uk-%2fN}TLa#26G7GwIe=z^_@=)U}jgY-sd>X!fy#aX* zmM56=9c~xNezvNqI3uRfySPHc?Qwxb>XT`G%ck_)7yk3QHuuJ{`lX)P-*{@~G(UCA zc%FY_lD&|exKD0b-9yb4A5yP+{0gZ()fBn?i+eoxiTC~Y9(%2O?J=+Df1e5G;!HP= z-s2@EJ0(^cXJ|>^=>GM7sbS`-Z#(jCzq7Mz++DUz?#B(zaEICN4%y2a{bdXAX6N9( zV<(cu$iQ%iiGcwcBW#c;nS7H;xjsJskb=n3``CrXmH#A_d;6i{NtBxzt8!j7jxp(ovjx+*G!I@s?jSf()cLKBXs(U#Z`0l0-o3e zH*Jk_?9_X3q3hnn-m7eDraxHZ5U|dr_j_9B#>c!RXO12WJ7}44+faVV+KqNHD(&KD z>-%Lj`Zud+8qBfPU`)zlQ+U;Rdh&n$kc_&$^KXZV3s+1DxiE8DT7gvo?j@ z#Mh}l;K{`qVMWgld%cLNi>%mv_CTau`bV32zSn|e&;LH}x>5hxMU9R8{Xf{=70y$g zynJ!a(Gs^066LA+zjH5}^iM9xYrHRabAyzOFXy{J?oF4&SY)MYUFS)sNq$%K|IFUx zUX=27N&Y?F$BhAJjxC%h=oT3kohIe}e*I@Q(IX!uzp#UXRdd~zN)uqPDj|Xu8mitb zS3wDVvofnRIK^*vWIM}L|JG+iSy$}Vmp-mnthVpHK4o6jpLeYnLbfe)3tZ%FxI<*k z!j`i=`Y zf51G<&LPS6zb}!Qfx$u$o`N7=-F%z#0~6S<=XgpXx!aLVY;z1>62#*N`QsqcN1>6ObWkij@GJ&Gfj8`V- zipqg`eL&vv$*V*a!Mt-op8e!kq6%Ohub4E`F8;|E_{I4HycwB97~n{4zS%&e_Ivn*ea=56f~6BWXW8yN zk`isQ?`qNURROcMN#EYT{XjzS^dDvCZI?fe9{TOMApLxoqSu6(Z&q2QJCj zJ8O6OiHmJrbEa)=5Rr9U99cHmz%yd)1J|oUQywmm%~mlW_dWkiS{sHOU%}>|!Vk6_`K~f!pZbsMzpriRug{*k^^N2W3-+@j z(~sTTpkA+{qqkC{tn8sANBQ12D#@#CQ$4pv-dLP(@r2*~b)l+xNyyxy|9vH|e?8Rd z@RXR`S#NkL;X-KGhUahZpKe;Q+Un7^+`Yelu%!K(>6Vau*q5`misOIHi8s;#-s~Jo zqK6-4F)}dRVPar_#t0iEN+#cAQm#LpcgR4%_50th6G`54*aKQ*7TaAB4VpO1sr*D~ z+mXSfQcn z&2kl#&^IfyN`q7UW=FQOO!adU-rn$iz2RHj=ZaS|cZ)j(-<&1o+NpQ+f!c;Pph@W@IJ=~JZ{5X0?Hl}Uwdc7G++h##bLQ>YH*um)lxf^zpJw6I7EqaQ_F8&ko5f?R@}|u#=Vqu?o=MxHuC4R@+do6~OV0#v#s{tV=YIT$ImnYM z|6a9;W@cc}6@;f9h$lBctCHHrc0`7-n6ifzDv z&*x%b;AcPv=QtS{%JYj-^ovU>b5e`-DspoIycwB97~lm_)H$8rRtsRv?FV{H2w5+~ z!O0s$73Dz|-0Z9