Starting docx reader library, doesn't working very well
This commit is contained in:
commit
d43836e26f
48
.gitignore
vendored
Normal file
48
.gitignore
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
# Miscellaneous
|
||||
*.class
|
||||
*.log
|
||||
*.pyc
|
||||
*.swp
|
||||
.DS_Store
|
||||
.atom/
|
||||
.build/
|
||||
.buildlog/
|
||||
.history
|
||||
.svn/
|
||||
.swiftpm/
|
||||
migrate_working_dir/
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
*.ipr
|
||||
*.iws
|
||||
.idea/
|
||||
|
||||
# The .vscode folder contains launch configuration and tasks you configure in
|
||||
# VS Code which you may wish to be included in version control, so this line
|
||||
# is commented out by default.
|
||||
#.vscode/
|
||||
|
||||
# Flutter/Dart/Pub related
|
||||
**/doc/api/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
*/build/*
|
||||
|
||||
# Symbolication related
|
||||
app.*.symbols
|
||||
|
||||
# Obfuscation related
|
||||
app.*.map.json
|
||||
|
||||
# Android Studio will place build artifacts here
|
||||
/android/app/debug
|
||||
/android/app/profile
|
||||
/android/app/release
|
||||
|
||||
# Useless folder/file
|
||||
pubspec.lock
|
37
analysis_options.yaml
Normal file
37
analysis_options.yaml
Normal file
|
@ -0,0 +1,37 @@
|
|||
# This file configures the static analysis results for your project (errors,
|
||||
# warnings, and lints).
|
||||
#
|
||||
# This enables the 'recommended' set of lints from `package:lints`.
|
||||
# This set helps identify many issues that may lead to problems when running
|
||||
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
|
||||
# style and format.
|
||||
#
|
||||
# If you want a smaller set of lints you can change this to specify
|
||||
# 'package:lints/core.yaml'. These are just the most critical lints
|
||||
# (the recommended set includes the core lints).
|
||||
# The core lints are also what is used by pub.dev for scoring packages.
|
||||
|
||||
include: package:lints/recommended.yaml
|
||||
|
||||
analyzer:
|
||||
errors:
|
||||
curly_braces_in_flow_control_structures: ignore
|
||||
avoid_print: ignore
|
||||
use_build_context_synchronously: ignore
|
||||
prefer_initializing_formals: ignore
|
||||
|
||||
# Uncomment the following section to specify additional rules.
|
||||
|
||||
# linter:
|
||||
# rules:
|
||||
# - camel_case_types
|
||||
|
||||
# analyzer:
|
||||
# exclude:
|
||||
# - path/to/excluded/files/**
|
||||
|
||||
# For more information about the core and recommended set of lints, see
|
||||
# https://dart.dev/go/core-lints
|
||||
|
||||
# For additional information about configuring this file, see
|
||||
# https://dart.dev/guides/language/analysis-options
|
13
bin/main.dart
Normal file
13
bin/main.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
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))
|
||||
});
|
||||
}
|
175
lib/docx.dart
Normal file
175
lib/docx.dart
Normal file
|
@ -0,0 +1,175 @@
|
|||
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 fileDocx;
|
||||
|
||||
/// [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)));
|
||||
|
||||
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 = fileDocx.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: ' '));
|
||||
|
||||
// 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!);
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// Replaces the text ([node]) with an image ([replaceContent])
|
||||
XmlNode? putImage(XmlElement node, ReplaceContent replaceContent, XmlNode? newParent) {
|
||||
|
||||
if (node.parent == null && newParent != null)
|
||||
node.attachParent(newParent);
|
||||
XmlNode? parent = node.parent;
|
||||
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;
|
||||
}
|
||||
|
||||
/// Recreate a Archive to add modification and save it on [fileDocx]
|
||||
/// [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 originalBytes = fileDocx.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);
|
||||
fileDocx.writeAsBytesSync(zipData);
|
||||
} catch (e) {
|
||||
throw StateError('DOCX BUILDER ERROR: $e');
|
||||
}
|
||||
}
|
||||
}
|
91
lib/replace.dart
Normal file
91
lib/replace.dart
Normal file
|
@ -0,0 +1,91 @@
|
|||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
|
||||
class ReplaceContent {
|
||||
DocxImage? img;
|
||||
String? value; /// Only Text to replace; null if it's image!
|
||||
|
||||
ReplaceContent({this.value, this.img}) {
|
||||
if (img != null && value != null)
|
||||
throw StateError('Can\'t have value and image define');
|
||||
if (img == null && value == null)
|
||||
throw StateError('You need define value or image');
|
||||
}
|
||||
}
|
||||
|
||||
/// 16cm * 360 000 = 5 760 000 EMU
|
||||
const int maxWidth = 16 * 360000;
|
||||
/// 24cm * 360 000 = 8 640 000 EMU
|
||||
const int maxHeight = 24 * 360000;
|
||||
|
||||
class DocxImage {
|
||||
|
||||
int? id;
|
||||
int? sizeX; /// in EMU
|
||||
int? sizeY; /// in EMU
|
||||
late File file;
|
||||
|
||||
/// [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;
|
||||
|
||||
if (sizeX == null && sizeY == null) {
|
||||
Uint8List bytes = file.readAsBytesSync();
|
||||
String extensions = file.path.substring(file.path.lastIndexOf('.') + 1);
|
||||
|
||||
if (extensions == 'png') {
|
||||
sizeX = (bytes.buffer.asByteData().getUint32(16)) * 9525;
|
||||
sizeY = (bytes.buffer.asByteData().getUint32(20)) * 9525;
|
||||
}
|
||||
else if (extensions == 'jpg' || extensions == 'jpeg') {
|
||||
List<int> sizes = getSizeJpg(bytes);
|
||||
if (sizes.isEmpty)
|
||||
throw StateError('Can\'t get the size of Jpg image');
|
||||
sizeX = sizes[0];
|
||||
sizeY = sizes[1];
|
||||
}
|
||||
}
|
||||
else if (sizeX != null && sizeY != null) {
|
||||
sizeX = sizeX! * 9525;
|
||||
sizeY = sizeY! * 9525;
|
||||
}
|
||||
else
|
||||
throw StateError('Only sizeX or sizeY is defined!');
|
||||
|
||||
// Limit size of image
|
||||
double scale = min(maxWidth / sizeX!, maxHeight / sizeY!);
|
||||
scale = scale > 1 ? 1 : scale;
|
||||
sizeX = (sizeX! * scale).round();
|
||||
sizeY = (sizeY! * scale).round();
|
||||
}
|
||||
}
|
||||
|
||||
/// Return size of Jpg => [width, height]
|
||||
List<int> getSizeJpg(Uint8List bytes) {
|
||||
int i = 0;
|
||||
while (i < bytes.length) {
|
||||
while(bytes[i]==0xff)
|
||||
i++;
|
||||
int marker = bytes[i++];
|
||||
|
||||
if(marker==0xd8) // SOI
|
||||
continue;
|
||||
if(marker==0xd9) // EOI
|
||||
break;
|
||||
if(0xd0<=marker && marker<=0xd7)
|
||||
continue;
|
||||
if(marker==0x01) // TEM
|
||||
continue;
|
||||
|
||||
int length = (bytes[i] << 8) | bytes[i + 1];
|
||||
i += 2;
|
||||
if (marker == 0xc0)
|
||||
return [(bytes[i + 1] << 8) + bytes[i + 2], // Width
|
||||
(bytes[i + 3] << 8) + bytes[i + 4]]; // Height
|
||||
|
||||
i += length - 2;
|
||||
}
|
||||
return [];
|
||||
}
|
18
pubspec.yaml
Normal file
18
pubspec.yaml
Normal file
|
@ -0,0 +1,18 @@
|
|||
publish_to: 'none'
|
||||
name: docx
|
||||
description: A sample command-line application.
|
||||
version: 1.0.0
|
||||
# repository: https://github.com/my_org/my_repo
|
||||
|
||||
environment:
|
||||
sdk: ^3.8.1
|
||||
|
||||
# Add regular dependencies here.
|
||||
dependencies:
|
||||
xml: ^6.6.0
|
||||
archive: ^4.0.7
|
||||
# path: ^1.8.0
|
||||
|
||||
dev_dependencies:
|
||||
lints: ^6.0.0
|
||||
test: ^1.24.0
|
BIN
test/assets/cat.png
Normal file
BIN
test/assets/cat.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 MiB |
BIN
test/assets/docx_test.docx
Normal file
BIN
test/assets/docx_test.docx
Normal file
Binary file not shown.
BIN
test/assets/docx_test_copy.docx
Normal file
BIN
test/assets/docx_test_copy.docx
Normal file
Binary file not shown.
BIN
test/assets/dog.jpg
Normal file
BIN
test/assets/dog.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 217 KiB |
BIN
test/assets/dog.png
Normal file
BIN
test/assets/dog.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 MiB |
17
test/docx_test.dart
Normal file
17
test/docx_test.dart
Normal file
|
@ -0,0 +1,17 @@
|
|||
import 'package:docx/docx.dart';
|
||||
import 'package:docx/replace.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'dart:io';
|
||||
|
||||
void main() {
|
||||
test('Try to generate a file without error', () {
|
||||
DocxEditor docx = DocxEditor(File('test/assets/docx_test.docx'), {
|
||||
'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)),
|
||||
'Footer': ReplaceContent(img: DocxImage(file: File('test/assets/dog.jpg'), sizeX: 400, sizeY: 300))
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in a new issue