diff --git a/.gitignore b/.gitignore index 7762dbd..b92f33c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,7 @@ *.swp .DS_Store .atom/ -.build/ +build/ .buildlog/ .history .svn/ @@ -46,3 +46,5 @@ app.*.map.json # Useless folder/file pubspec.lock + +docx_test.docx diff --git a/lib/docx.dart b/lib/docx.dart index 6935221..7f4b598 100644 --- a/lib/docx.dart +++ b/lib/docx.dart @@ -1,213 +1,4 @@ -import 'dart:collection'; -import 'dart:convert'; - -import 'dart:io'; -import 'dart:typed_data'; -import 'package:archive/archive.dart'; -import 'package:xml/xml.dart'; -import 'package:docx/replace.dart'; - -class DocxEditor { - - File fileInDocx; - String? pathOutDocx; - - /// [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); - } - - /// This function search ID in [replaceMap] and replace by his value or by a image - List editById(Map replaceMap) { - 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)); - - 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; - 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 - File save(List documentsXml, Map replaceMap) { - try { - final originalBytes = fileInDocx.readAsBytesSync(); - final archive = ZipDecoder().decodeBytes(originalBytes); - - 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!.file.readAsBytesSync(); - newArchive.addFile( - ArchiveFile( - 'word/media/${replaceCnt.img!.file.path}', - imageBytes.length, - imageBytes, - ) - ); - } - - final zipData = ZipEncoder().encode(newArchive); - - File newFile = pathOutDocx == null ? fileInDocx : File(pathOutDocx!); - newFile.writeAsBytesSync(zipData); - return newFile; - - } catch (e) { - throw StateError('DOCX BUILDER ERROR: $e'); - } - } -} +export 'src/replace.dart'; +export 'src/docx.dart' + if (dart.library.js_interop) 'src/docx_web.dart' + if (dart.library.io) 'src/docx.dart'; \ No newline at end of file diff --git a/lib/src/docx.dart b/lib/src/docx.dart new file mode 100644 index 0000000..8cedf26 --- /dev/null +++ b/lib/src/docx.dart @@ -0,0 +1,225 @@ +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'); + } + } +} diff --git a/lib/src/docx_web.dart b/lib/src/docx_web.dart new file mode 100644 index 0000000..b50a7c2 --- /dev/null +++ b/lib/src/docx_web.dart @@ -0,0 +1,222 @@ +import 'dart:convert'; + +// Web +import 'package:web/web.dart' as web; +import 'dart:js_interop'; + +import 'dart:typed_data'; +import 'package:archive/archive.dart'; +import 'package:xml/xml.dart'; +import 'package:docx/docx.dart'; + +class DocxEditor { + + Uint8List binaryInDocx; + String? pathOutDocx; + String? name; + + /// [binaryInDocx] The Docx binary (required) + /// [pathOutDocx] The path where new file is save (optional) + /// [replaceMap] All text and image to change (required) + DocxEditor({ + required this.binaryInDocx, + this.name, + this.pathOutDocx, + required Map replaceMap + }) { + + 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, + ) + ); + } + + String url = web.URL.createObjectURL( + web.Blob( + [binaryInDocx.toJS].toJS, + web.BlobPropertyBag(type: 'application/octet-stream'), + ), + ); + web.Document htmlDocument = web.document; + web.HTMLAnchorElement anchor = htmlDocument.createElement('a') as web.HTMLAnchorElement; + anchor.href = url; + anchor.style.display = '${name ?? 'template'}.docx'; + anchor.download = '${name ?? 'template'}.docx'; + web.document.body!.add(anchor); + anchor.click(); + anchor.remove(); + + } catch (e) { + throw StateError('DOCX BUILDER ERROR: $e'); + } + } +} diff --git a/lib/replace.dart b/lib/src/replace.dart similarity index 88% rename from lib/replace.dart rename to lib/src/replace.dart index 437547b..7d381cf 100644 --- a/lib/replace.dart +++ b/lib/src/replace.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:typed_data'; import 'dart:math'; @@ -24,16 +23,14 @@ class DocxImage { int? id; int? sizeX; /// in EMU int? sizeY; /// in EMU - late File file; + String name; // name of the image ['exemple.png', 'exemple.jpg'. 'exemple.jpeg'] + late Uint8List bytes; // Bytes of image - /// [file] support JPG/JPEG and PNG /// [sizeX] and [sizeY] use EMU metrics BUT init a instance in PIXEL - DocxImage({required File file, this.sizeX, this.sizeY}) { - this.file = file; + DocxImage({required this.bytes, required this.name, this.sizeX, this.sizeY}) { if (sizeX == null && sizeY == null) { - Uint8List bytes = file.readAsBytesSync(); - String extensions = file.path.substring(file.path.lastIndexOf('.') + 1); + String extensions = name.substring(name.lastIndexOf('.') + 1); if (extensions == 'png') { sizeX = (bytes.buffer.asByteData().getUint32(16)) * 9525; @@ -88,4 +85,4 @@ List getSizeJpg(Uint8List bytes) { i += length - 2; } return []; -} \ No newline at end of file +} diff --git a/pubspec.yaml b/pubspec.yaml index 766cdc1..e352aed 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: xml: ^6.6.0 archive: ^4.0.7 xml2json: ^6.2.7 - # path: ^1.8.0 + web: ^1.1.1 dev_dependencies: lints: ^6.0.0 diff --git a/test/docx_test.dart b/test/docx_test.dart index 23241de..9aed9b9 100644 --- a/test/docx_test.dart +++ b/test/docx_test.dart @@ -1,21 +1,27 @@ +import 'dart:typed_data'; + import 'package:docx/docx.dart'; -import 'package:docx/replace.dart'; import 'package:test/test.dart'; import 'dart:io'; +// Test only on desktop void main() { test('Try to generate a file without error', () { - DocxEditor docx = DocxEditor( + + Uint8List catImg = File('test/assets/cat.png').readAsBytesSync(); + Uint8List dogImg = File('test/assets/dog.jpg').readAsBytesSync(); + + DocxEditor( fileInDocx: File('test/assets/docx_test_copy.docx'), pathOutDocx: 'test/assets/docx_test.docx', replaceMap: { 'Title': ReplaceContent(value: 'NewTitle'), 'A': ReplaceContent(value: 'AModified'), 'Image-Dog': ReplaceContent(value: 'NotImage-Dog'), - 'ImageCat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), - 'Image-Cat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)), - 'Footer': ReplaceContent(img: DocxImage(file: File('test/assets/dog.jpg'), sizeX: 400, sizeY: 300)) + 'ImageCat': ReplaceContent(img: DocxImage(bytes: catImg, name: 'cat.png' , sizeX: 200, sizeY: 400)), + 'Image-Cat': ReplaceContent(img: DocxImage(bytes: catImg, name: 'cat.png' , sizeX: 200, sizeY: 400)), + 'Footer': ReplaceContent(img: DocxImage(bytes: dogImg, name: 'dog.jpg', sizeX: 400, sizeY: 300)) } ); });