import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'package:archive/archive.dart'; import 'package:xml/xml.dart'; import 'package:docx/docx.dart'; class DocxEditor { File? fileInDocx; Uint8List? binaryInDocx; String? pathOutDocx; String? name; /// [fileInDocx] The Docx file /// [binaryInDocx] The Docx binary (for web generaly) /// [pathOutDocx] The path where new file is save (optional) /// [replaceMap] All text and image to change (required) DocxEditor({ this.fileInDocx, this.binaryInDocx, this.name, this.pathOutDocx, required Map replaceMap }) { if (fileInDocx != null) { if (!fileInDocx!.existsSync()) throw(PathNotFoundException(fileInDocx!.path, OSError('File not found', 404))); binaryInDocx = fileInDocx!.readAsBytesSync(); } else if (binaryInDocx == null) throw(ArgumentError('file or binary is mandatory to be define')); List documentsXml = editById(replaceMap); save(documentsXml, replaceMap); } /// This function search ID in [replaceMap] and replace by his value or by a image List editById(Map replaceMap) { Archive archive = ZipDecoder().decodeBytes(binaryInDocx!); 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); //print(documentXml.toXmlString(pretty: true, indent: ' ')); // 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; // ignore: unused_local_variable 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]; } // TODO: Find a solution to put multiple image in only one /// Replaces the text ([node]) with an image ([replaceContent]) XmlElement? putImage(XmlElement? node, ReplaceContent replaceContent, XmlElement? newParent) { if (node!.parentElement == null && newParent != null) node.attachParent(newParent); XmlElement? parent = node.parentElement; String nodeStr = node.toXmlString(); node.replace(XmlDocumentFragment.parse(""" $nodeStr """)); return parent; } /// 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) { try { final archive = ZipDecoder().decodeBytes(binaryInDocx!); final Uint8List uListDocumentXml = utf8.encode(documentsXml[0].toXmlString()); final Uint8List uListDocumentXmlRels = utf8.encode(documentsXml[1].toXmlString()); final newArchive = Archive(); for (ArchiveFile file in archive.files) { if (file.name == 'word/document.xml') { newArchive.addFile(ArchiveFile( 'word/document.xml', uListDocumentXml.length, uListDocumentXml, )); } else if (file.name == 'word/_rels/document.xml.rels') { newArchive.addFile(ArchiveFile( 'word/_rels/document.xml.rels', uListDocumentXmlRels.length, uListDocumentXmlRels, )); } else { newArchive.addFile(file); } } for (ReplaceContent replaceCnt in replaceMap.values.where((rm) => rm.img != null)) { final imageBytes = replaceCnt.img!.bytes; newArchive.addFile( ArchiveFile( 'word/media/${replaceCnt.img!.name}', imageBytes.length, imageBytes, ) ); } final zipData = ZipEncoder().encode(newArchive); if (fileInDocx != null) { File newFile = pathOutDocx == null ? fileInDocx! : File(pathOutDocx!); newFile.writeAsBytesSync(zipData); } else { File newFile = pathOutDocx == null ? File('.') : File(pathOutDocx!); newFile.writeAsBytesSync(zipData); } } catch (e) { throw StateError('DOCX BUILDER ERROR: $e'); } } }