commit d43836e26fe53aeeef3b966ec02c4720c4d2b8b3 Author: Xamora Date: Thu Jul 3 08:39:39 2025 +0200 Starting docx reader library, doesn't working very well diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7762dbd --- /dev/null +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..b504558 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# Docx Editor Library + +IN WIP \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..b89cc58 --- /dev/null +++ b/analysis_options.yaml @@ -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 diff --git a/bin/main.dart b/bin/main.dart new file mode 100644 index 0000000..75a2699 --- /dev/null +++ b/bin/main.dart @@ -0,0 +1,13 @@ +import 'package:docx/docx.dart'; +import 'dart:io'; +import 'package:docx/replace.dart'; + +void main(List 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)) + }); +} diff --git a/lib/docx.dart b/lib/docx.dart new file mode 100644 index 0000000..8811d54 --- /dev/null +++ b/lib/docx.dart @@ -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 replaceMap) { + if (!fileDocx.existsSync()) + throw(PathNotFoundException(fileDocx.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 = 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)); + + 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 () et non les runs () + for (XmlElement node in documentXml.findAllElements('w:r')) { + String ndText = node.innerText.toLowerCase(); + //Iterable replacesId = replaceMap.keys.where((rm) => rm.toLowerCase() == ndText); + for (String key in replaceMap.keys) { + Iterable 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(''); + int end = node.innerXml.indexOf(''); + 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 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; + } + + /// 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + """)); + 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 documentsXml, Map 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'); + } + } +} \ No newline at end of file diff --git a/lib/replace.dart b/lib/replace.dart new file mode 100644 index 0000000..437547b --- /dev/null +++ b/lib/replace.dart @@ -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 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 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 []; +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..7649126 --- /dev/null +++ b/pubspec.yaml @@ -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 diff --git a/test/assets/cat.png b/test/assets/cat.png new file mode 100644 index 0000000..30302fe Binary files /dev/null and b/test/assets/cat.png differ diff --git a/test/assets/docx_test.docx b/test/assets/docx_test.docx new file mode 100644 index 0000000..1479137 Binary files /dev/null and b/test/assets/docx_test.docx differ diff --git a/test/assets/docx_test_copy.docx b/test/assets/docx_test_copy.docx new file mode 100644 index 0000000..c676b5d Binary files /dev/null and b/test/assets/docx_test_copy.docx differ diff --git a/test/assets/dog.jpg b/test/assets/dog.jpg new file mode 100644 index 0000000..aba8892 Binary files /dev/null and b/test/assets/dog.jpg differ diff --git a/test/assets/dog.png b/test/assets/dog.png new file mode 100644 index 0000000..d089a2e Binary files /dev/null and b/test/assets/dog.png differ diff --git a/test/docx_test.dart b/test/docx_test.dart new file mode 100644 index 0000000..6b53026 --- /dev/null +++ b/test/docx_test.dart @@ -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)) + }); + }); +}