starting project, replace text with text or image

This commit is contained in:
Xamora 2025-07-09 14:36:11 +02:00
parent d43836e26f
commit cb71e72a2a
7 changed files with 110 additions and 77 deletions

View file

@ -1,3 +1,5 @@
# Docx Editor Library
IN WIP
IN WIP
0.0.1

View file

@ -1,13 +0,0 @@
import 'package:docx/docx.dart';
import 'dart:io';
import 'package:docx/replace.dart';
void main(List<String> 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))
});
}

View file

@ -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<String, ReplaceContent> 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<String, ReplaceContent> replaceMap
}) {
if (!fileInDocx.existsSync())
throw(PathNotFoundException(fileInDocx.path, OSError('File not found', 404)));
List<XmlDocument> 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<XmlDocument> editById(Map<String, ReplaceContent> 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<int>));
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<int>));
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 (<w:p>) et non les runs (<w:r>)
for (XmlElement node in documentXml.findAllElements('w:r')) {
String ndText = node.innerText.toLowerCase();
//Iterable<String> replacesId = replaceMap.keys.where((rm) => rm.toLowerCase() == ndText);
for (String key in replaceMap.keys) {
Iterable<String> 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('<w:t>');
int end = node.innerXml.indexOf('</w:t>');
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<XmlElement> 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<String, ReplaceContent> replaceMap) {
ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels');
XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List<int>));
String documentXmlStr = documentXmlRels.toXmlString();
RegExp exp = RegExp(r'Id="rId([0-9]+)"');
Iterable<RegExpMatch> 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 = '<Relationship Id="rId$maxId" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${replaceCnt.img!.file.path}"/>\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 <w:r>
/// 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<String, ReplaceContent> replaceMap) {
ArchiveFile archiveFile = archive.firstWhere((f) => f.name == 'word/_rels/document.xml.rels');
XmlDocument documentXmlRels = XmlDocument.parse(utf8.decode(archiveFile.content as List<int>));
String documentXmlStr = documentXmlRels.toXmlString();
RegExp exp = RegExp(r'Id="rId([0-9]+)"');
Iterable<RegExpMatch> 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 = '<Relationship Id="rId$maxId" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/${replaceCnt.img!.file.path}"/>\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<XmlDocument> documentsXml, Map<String, ReplaceContent> replaceMap) {
File save(List<XmlDocument> documentsXml, Map<String, ReplaceContent> 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');
}
}
}
}

View file

@ -11,6 +11,7 @@ environment:
dependencies:
xml: ^6.6.0
archive: ^4.0.7
xml2json: ^6.2.7
# path: ^1.8.0
dev_dependencies:

Binary file not shown.

Binary file not shown.

View file

@ -6,12 +6,17 @@ import 'dart:io';
void main() {
test('Try to generate a file without error', () {
DocxEditor docx = DocxEditor(File('test/assets/docx_test.docx'), {
DocxEditor docx = 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-Chat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)),
'Image-Dog': ReplaceContent(img: DocxImage(file: File('test/assets/dog.png'), sizeX: 400, sizeY: 300)),
'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))
});
}
);
});
}