compability between desktop and web
This commit is contained in:
parent
cb71e72a2a
commit
d09241852c
4
.gitignore
vendored
4
.gitignore
vendored
|
@ -5,7 +5,7 @@
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.atom/
|
.atom/
|
||||||
.build/
|
build/
|
||||||
.buildlog/
|
.buildlog/
|
||||||
.history
|
.history
|
||||||
.svn/
|
.svn/
|
||||||
|
@ -46,3 +46,5 @@ app.*.map.json
|
||||||
|
|
||||||
# Useless folder/file
|
# Useless folder/file
|
||||||
pubspec.lock
|
pubspec.lock
|
||||||
|
|
||||||
|
docx_test.docx
|
||||||
|
|
217
lib/docx.dart
217
lib/docx.dart
|
@ -1,213 +1,4 @@
|
||||||
import 'dart:collection';
|
export 'src/replace.dart';
|
||||||
import 'dart:convert';
|
export 'src/docx.dart'
|
||||||
|
if (dart.library.js_interop) 'src/docx_web.dart'
|
||||||
import 'dart:io';
|
if (dart.library.io) 'src/docx.dart';
|
||||||
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<String, ReplaceContent> replaceMap
|
|
||||||
}) {
|
|
||||||
if (!fileInDocx.existsSync())
|
|
||||||
throw(PathNotFoundException(fileInDocx.path, OSError('File not found', 404)));
|
|
||||||
|
|
||||||
List<XmlDocument> documentsXml = editById(replaceMap);
|
|
||||||
save(documentsXml, replaceMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This function search ID in [replaceMap] and replace by his value or by a image
|
|
||||||
List<XmlDocument> editById(Map<String, ReplaceContent> 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<int>));
|
|
||||||
|
|
||||||
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<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];
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Find a solution to put multiple image in only one <w:r>
|
|
||||||
/// 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
|
|
||||||
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
|
||||||
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
|
||||||
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
|
||||||
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
|
||||||
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
|
||||||
<w:drawing>
|
|
||||||
<wp:inline distT="0" distB="0" distL="0" distR="0">
|
|
||||||
<wp:extent cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
|
|
||||||
<wp:docPr id="1" name="Image1"/>
|
|
||||||
<wp:cNvGraphicFramePr/>
|
|
||||||
<a:graphic>
|
|
||||||
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
|
||||||
<pic:pic>
|
|
||||||
<pic:blipFill>
|
|
||||||
<a:blip r:embed="rId${replaceContent.img!.id}"/>
|
|
||||||
<a:stretch>
|
|
||||||
<a:fillRect/>
|
|
||||||
</a:stretch>
|
|
||||||
</pic:blipFill>
|
|
||||||
<pic:spPr>
|
|
||||||
<a:xfrm>
|
|
||||||
<a:off x="0" y="0"/>
|
|
||||||
<a:ext cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
|
|
||||||
</a:xfrm>
|
|
||||||
<a:prstGeom prst="rect">
|
|
||||||
<a:avLst/>
|
|
||||||
</a:prstGeom>
|
|
||||||
</pic:spPr>
|
|
||||||
</pic:pic>
|
|
||||||
</a:graphicData>
|
|
||||||
</a:graphic>
|
|
||||||
</wp:inline>
|
|
||||||
</w:drawing>
|
|
||||||
</w:r>
|
|
||||||
"""));
|
|
||||||
return parent;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
File save(List<XmlDocument> documentsXml, Map<String, ReplaceContent> 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
225
lib/src/docx.dart
Normal file
225
lib/src/docx.dart
Normal file
|
@ -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<String, ReplaceContent> 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<XmlDocument> documentsXml = editById(replaceMap);
|
||||||
|
save(documentsXml, replaceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function search ID in [replaceMap] and replace by his value or by a image
|
||||||
|
List<XmlDocument> editById(Map<String, ReplaceContent> 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<int>));
|
||||||
|
|
||||||
|
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<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];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Find a solution to put multiple image in only one <w:r>
|
||||||
|
/// 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
|
||||||
|
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||||
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||||
|
<w:drawing>
|
||||||
|
<wp:inline distT="0" distB="0" distL="0" distR="0">
|
||||||
|
<wp:extent cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
|
||||||
|
<wp:docPr id="1" name="Image1"/>
|
||||||
|
<wp:cNvGraphicFramePr/>
|
||||||
|
<a:graphic>
|
||||||
|
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||||
|
<pic:pic>
|
||||||
|
<pic:blipFill>
|
||||||
|
<a:blip r:embed="rId${replaceContent.img!.id}"/>
|
||||||
|
<a:stretch>
|
||||||
|
<a:fillRect/>
|
||||||
|
</a:stretch>
|
||||||
|
</pic:blipFill>
|
||||||
|
<pic:spPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="0" y="0"/>
|
||||||
|
<a:ext cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
|
||||||
|
</a:xfrm>
|
||||||
|
<a:prstGeom prst="rect">
|
||||||
|
<a:avLst/>
|
||||||
|
</a:prstGeom>
|
||||||
|
</pic:spPr>
|
||||||
|
</pic:pic>
|
||||||
|
</a:graphicData>
|
||||||
|
</a:graphic>
|
||||||
|
</wp:inline>
|
||||||
|
</w:drawing>
|
||||||
|
</w:r>
|
||||||
|
"""));
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!.name}"/>\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) {
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
222
lib/src/docx_web.dart
Normal file
222
lib/src/docx_web.dart
Normal file
|
@ -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<String, ReplaceContent> replaceMap
|
||||||
|
}) {
|
||||||
|
|
||||||
|
List<XmlDocument> documentsXml = editById(replaceMap);
|
||||||
|
save(documentsXml, replaceMap);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This function search ID in [replaceMap] and replace by his value or by a image
|
||||||
|
List<XmlDocument> editById(Map<String, ReplaceContent> 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<int>));
|
||||||
|
|
||||||
|
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<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];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Find a solution to put multiple image in only one <w:r>
|
||||||
|
/// 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
|
||||||
|
<w:r xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||||
|
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"
|
||||||
|
xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing"
|
||||||
|
xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"
|
||||||
|
xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||||
|
<w:drawing>
|
||||||
|
<wp:inline distT="0" distB="0" distL="0" distR="0">
|
||||||
|
<wp:extent cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
|
||||||
|
<wp:docPr id="1" name="Image1"/>
|
||||||
|
<wp:cNvGraphicFramePr/>
|
||||||
|
<a:graphic>
|
||||||
|
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
||||||
|
<pic:pic>
|
||||||
|
<pic:blipFill>
|
||||||
|
<a:blip r:embed="rId${replaceContent.img!.id}"/>
|
||||||
|
<a:stretch>
|
||||||
|
<a:fillRect/>
|
||||||
|
</a:stretch>
|
||||||
|
</pic:blipFill>
|
||||||
|
<pic:spPr>
|
||||||
|
<a:xfrm>
|
||||||
|
<a:off x="0" y="0"/>
|
||||||
|
<a:ext cx="${replaceContent.img!.sizeX!}" cy="${replaceContent.img!.sizeY!}"/>
|
||||||
|
</a:xfrm>
|
||||||
|
<a:prstGeom prst="rect">
|
||||||
|
<a:avLst/>
|
||||||
|
</a:prstGeom>
|
||||||
|
</pic:spPr>
|
||||||
|
</pic:pic>
|
||||||
|
</a:graphicData>
|
||||||
|
</a:graphic>
|
||||||
|
</wp:inline>
|
||||||
|
</w:drawing>
|
||||||
|
</w:r>
|
||||||
|
"""));
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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!.name}"/>\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) {
|
||||||
|
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(
|
||||||
|
<JSUint8Array>[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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,3 @@
|
||||||
import 'dart:io';
|
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
|
|
||||||
|
@ -24,16 +23,14 @@ class DocxImage {
|
||||||
int? id;
|
int? id;
|
||||||
int? sizeX; /// in EMU
|
int? sizeX; /// in EMU
|
||||||
int? sizeY; /// 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
|
/// [sizeX] and [sizeY] use EMU metrics BUT init a instance in PIXEL
|
||||||
DocxImage({required File file, this.sizeX, this.sizeY}) {
|
DocxImage({required this.bytes, required this.name, this.sizeX, this.sizeY}) {
|
||||||
this.file = file;
|
|
||||||
|
|
||||||
if (sizeX == null && sizeY == null) {
|
if (sizeX == null && sizeY == null) {
|
||||||
Uint8List bytes = file.readAsBytesSync();
|
String extensions = name.substring(name.lastIndexOf('.') + 1);
|
||||||
String extensions = file.path.substring(file.path.lastIndexOf('.') + 1);
|
|
||||||
|
|
||||||
if (extensions == 'png') {
|
if (extensions == 'png') {
|
||||||
sizeX = (bytes.buffer.asByteData().getUint32(16)) * 9525;
|
sizeX = (bytes.buffer.asByteData().getUint32(16)) * 9525;
|
|
@ -12,7 +12,7 @@ dependencies:
|
||||||
xml: ^6.6.0
|
xml: ^6.6.0
|
||||||
archive: ^4.0.7
|
archive: ^4.0.7
|
||||||
xml2json: ^6.2.7
|
xml2json: ^6.2.7
|
||||||
# path: ^1.8.0
|
web: ^1.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^6.0.0
|
lints: ^6.0.0
|
||||||
|
|
|
@ -1,21 +1,27 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:docx/docx.dart';
|
import 'package:docx/docx.dart';
|
||||||
import 'package:docx/replace.dart';
|
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
// Test only on desktop
|
||||||
void main() {
|
void main() {
|
||||||
test('Try to generate a file without error', () {
|
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'),
|
fileInDocx: File('test/assets/docx_test_copy.docx'),
|
||||||
pathOutDocx: 'test/assets/docx_test.docx',
|
pathOutDocx: 'test/assets/docx_test.docx',
|
||||||
replaceMap: {
|
replaceMap: {
|
||||||
'Title': ReplaceContent(value: 'NewTitle'),
|
'Title': ReplaceContent(value: 'NewTitle'),
|
||||||
'A': ReplaceContent(value: 'AModified'),
|
'A': ReplaceContent(value: 'AModified'),
|
||||||
'Image-Dog': ReplaceContent(value: 'NotImage-Dog'),
|
'Image-Dog': ReplaceContent(value: 'NotImage-Dog'),
|
||||||
'ImageCat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)),
|
'ImageCat': ReplaceContent(img: DocxImage(bytes: catImg, name: 'cat.png' , sizeX: 200, sizeY: 400)),
|
||||||
'Image-Cat': ReplaceContent(img: DocxImage(file: File('test/assets/cat.png'), sizeX: 200, sizeY: 400)),
|
'Image-Cat': ReplaceContent(img: DocxImage(bytes: catImg, name: 'cat.png' , sizeX: 200, sizeY: 400)),
|
||||||
'Footer': ReplaceContent(img: DocxImage(file: File('test/assets/dog.jpg'), sizeX: 400, sizeY: 300))
|
'Footer': ReplaceContent(img: DocxImage(bytes: dogImg, name: 'dog.jpg', sizeX: 400, sizeY: 300))
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue