Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Add support for reading/inserting images #45 #383

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions example/excel_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ void main(List<String> args) {
DateTimeCellValue() => 'date+time',
TimeCellValue() => 'time',
BoolCellValue() => 'bool',
ImageCellValue() => 'image',
});

///
Expand Down
3 changes: 3 additions & 0 deletions lib/excel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import 'dart:typed_data';
import 'package:archive/archive.dart';
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:excel/src/models/image_to_cell_model.dart';
import 'package:excel/src/models/relations_targets_models.dart';
import 'package:path/path.dart';
import 'package:xml/xml.dart';
import 'src/web_helper/client_save_excel.dart'
if (dart.library.html) 'src/web_helper/web_save_excel_browser.dart'
Expand Down
13 changes: 13 additions & 0 deletions lib/src/models/image_to_cell_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class ImageToCell {
final int row;
final int col;
final String imageId;
final String imageTarget;

const ImageToCell({
required this.row,
required this.col,
required this.imageId,
required this.imageTarget,
});
}
32 changes: 32 additions & 0 deletions lib/src/models/relations_targets_models.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class Relations {
final Map<String, String> _targets = {};

String? targetById(String id) => _targets[id];

@override
String toString() {
return 'Relations{_targets: $_targets}';
}
}

class RelationsByFile {
final Map<String, Relations> _relationsByFiles = {};

void addTarget(String fileName, String id, String target) {
final relationFile = _relationsByFiles[fileName] ??= Relations();
relationFile._targets[id] = target;
}

Relations? relations(String fileName) {
return _relationsByFiles[fileName];
}

String? target(String fileName, String id) {
return _relationsByFiles[fileName]?.targetById(id);
}

@override
String toString() {
return 'RelationsByFile{_relations: $_relationsByFiles}';
}
}
5 changes: 5 additions & 0 deletions lib/src/number_format/num_format.dart
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ sealed class NumFormat {
BoolCellValue() => NumFormat.defaultBool,
TimeCellValue() => NumFormat.defaultTime,
DateTimeCellValue() => NumFormat.defaultDateTime,
ImageCellValue() => NumFormat.defaultNumeric,
};
}

Expand Down Expand Up @@ -313,6 +314,7 @@ class StandardNumericNumFormat extends NumericNumFormat
DateCellValue() => false,
TimeCellValue() => false,
DateTimeCellValue() => false,
ImageCellValue() => false,
};

@override
Expand All @@ -338,6 +340,7 @@ class CustomNumericNumFormat extends NumericNumFormat
DateCellValue() => false,
TimeCellValue() => false,
DateTimeCellValue() => false,
ImageCellValue() => false,
};

@override
Expand Down Expand Up @@ -401,6 +404,7 @@ sealed class DateTimeNumFormat extends NumFormat {
DateCellValue() => true,
DateTimeCellValue() => true,
TimeCellValue() => false,
ImageCellValue() => false,
};
}

Expand Down Expand Up @@ -500,6 +504,7 @@ sealed class TimeNumFormat extends NumFormat {
DateCellValue() => false,
DateTimeCellValue() => false,
TimeCellValue() => true,
ImageCellValue() => false,
};
}

Expand Down
126 changes: 115 additions & 11 deletions lib/src/parser/parse.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ class Parser {
final Excel _excel;
final List<String> _rId = [];
final Map<String, String> _worksheetTargets = {};
final RelationsByFile _imageTargets = RelationsByFile();
final RelationsByFile _drawingTargets = RelationsByFile();

Parser._(this._excel);

Expand Down Expand Up @@ -36,21 +38,43 @@ class Parser {

void _parseRelations() {
var relations = _excel._archive.findFile('xl/_rels/workbook.xml.rels');
if (relations != null) {
if (relations == null) {
return _damagedExcel();
}

final relationsFiles =
_excel._archive.where((file) => file.name.contains('_rels'));

for (final relations in relationsFiles) {
final fileName = basenameWithoutExtension(relations.name);

relations.decompress();
var document = XmlDocument.parse(utf8.decode(relations.content));
_excel._xmlFiles['xl/_rels/workbook.xml.rels'] = document;
_excel._xmlFiles[relations.name] = document;

document.findAllElements('Relationship').forEach((node) {
String? id = node.getAttribute('Id');
String? target = node.getAttribute('Target');
final normalizedTarget = target?.replaceFirst('..', 'xl');

if (target != null) {
switch (node.getAttribute('Type')) {
case _relationshipsStyles:
_excel._stylesTarget = target;
break;
case _relationshipsWorksheet:
if (id != null) _worksheetTargets[id] = target;
break;
case _relationshipsImage:
if (id != null && normalizedTarget != null) {
_imageTargets.addTarget(fileName, id, normalizedTarget);
}
break;
case _relationshipsDrawing:
if (id != null && normalizedTarget != null) {
_drawingTargets.addTarget(fileName, id, normalizedTarget);
}

break;
case _relationshipsSharedStrings:
_excel._sharedStringsTarget = target;
Expand All @@ -61,8 +85,6 @@ class Parser {
_rId.add(id);
}
});
} else {
_damagedExcel();
}
}

Expand Down Expand Up @@ -526,6 +548,58 @@ class Parser {
return 0;
}

List<ImageToCell> _parseDrawings(String target, List<XmlElement> drawings) {
final targetFilename = basename(target);
var targetRelations = _drawingTargets.relations(targetFilename);

if (targetRelations == null)
throw "Something wrong with file. There are drawings, but no relations for them.";

final images = <ImageToCell>[];
drawings.forEach((drawing) {
final id = drawing.getAttribute('r:id')!;
final drawingTarget = targetRelations.targetById(id);

var sheetRelationsFile = _excel._archive.findFile(drawingTarget!);
sheetRelationsFile!.decompress();

var sheetRelationsContent =
XmlDocument.parse(utf8.decode(sheetRelationsFile.content));

final parsedImages = sheetRelationsContent
.findAllElements('xdr:oneCellAnchor')
.map((cell) {
final rawCol =
cell.findAllElements('xdr:col').firstOrNull?.innerText;
final rawRow =
cell.findAllElements('xdr:row').firstOrNull?.innerText;

final blip = cell.findAllElements('a:blip').firstOrNull;
final imageId = blip?.getAttribute('r:embed');

if (rawRow != null && rawCol != null && imageId != null) {
final col = int.parse(rawCol);
final row = int.parse(rawRow);

final imageTarget =
_imageTargets.target(basename(drawingTarget), imageId);

return ImageToCell(
row: row,
col: col,
imageId: imageId,
imageTarget: imageTarget!,
);
}
})
.nonNulls
.toList();

images.addAll(parsedImages);
});
return images;
}

void _parseTable(XmlElement node) {
var name = node.getAttribute('name')!;
var target = _worksheetTargets[node.getAttribute('r:id')];
Expand All @@ -541,6 +615,15 @@ class Parser {

var content = XmlDocument.parse(utf8.decode(file.content));
var worksheet = content.findElements('worksheet').first;
var drawings = worksheet.findAllElements('drawing').toList();

/// Theoretically worksheet could have more than one drawings.
/// In that case each image will be parsed from those drawings and displayed accordingly to their configs.
/// And theoretically image from one drawing could have same coordinates: we'll display the first we found.

final images = drawings.isNotEmpty
? _parseDrawings(target!, drawings)
: <ImageToCell>[];

///
/// check for right to left view
Expand All @@ -554,7 +637,7 @@ class Parser {
var sheet = worksheet.findElements('sheetData').first;

_findRows(sheet).forEach((child) {
_parseRow(child, sheetObject, name);
_parseRow(child, sheetObject, name, images);
});

_parseHeaderFooter(worksheet, sheetObject);
Expand All @@ -568,19 +651,30 @@ class Parser {
_normalizeTable(sheetObject);
}

_parseRow(XmlElement node, Sheet sheetObject, String name) {
void _parseRow(
XmlElement node,
Sheet sheetObject,
String name,
List<ImageToCell> images,
) {
var rowIndex = (_getRowNumber(node) ?? -1) - 1;
if (rowIndex < 0) {
return;
}
final rowImages = images.where((image) => image.row == rowIndex).toList();

_findCells(node).forEach((child) {
_parseCell(child, sheetObject, rowIndex, name);
_parseCell(child, sheetObject, rowIndex, name, rowImages);
});
}

void _parseCell(
XmlElement node, Sheet sheetObject, int rowIndex, String name) {
XmlElement node,
Sheet sheetObject,
int rowIndex,
String name,
List<ImageToCell> rowImages,
) {
int? columnIndex = _getCellNumber(node);
if (columnIndex == null) {
return;
Expand Down Expand Up @@ -637,7 +731,16 @@ class Parser {
value = FormulaCellValue(_parseValue(formulaNode.first).toString());
} else {
final vNode = node.findElements('v').firstOrNull;
if (vNode == null) {
final cellImage =
rowImages.firstWhereOrNull((image) => image.col == columnIndex);

if (cellImage != null) {
final imageFile = _excel._archive.firstWhereOrNull(
(file) => file.name.contains(cellImage.imageTarget));
imageFile?.decompress();

value = ImageCellValue(Uint8List.fromList(imageFile!.content));
} else if (vNode == null) {
value = null;
} else if (s1 != null) {
final v = _parseValue(vNode);
Expand Down Expand Up @@ -767,6 +870,7 @@ class Parser {
],
));

/// TODO smth must be done with [_imageTargets] and [_drawingTargets] in case of sheet creation
_worksheetTargets['rId$ridNumber'] = 'worksheets/sheet$sheetNumber.xml';

var content = utf8.encode(
Expand Down Expand Up @@ -842,8 +946,8 @@ class Parser {

/* parse custom column height
example XML content
<col min="2" max="2" width="71.83203125" customWidth="1"/>,
<col min="4" max="4" width="26.5" customWidth="1"/>,
<col min="2" max="2" width="71.83203125" customWidth="1"/>,
<col min="4" max="4" width="26.5" customWidth="1"/>,
<col min="6" max="6" width="31.33203125" customWidth="1"/>
*/
results = worksheet.findAllElements("col");
Expand Down
5 changes: 4 additions & 1 deletion lib/src/save/save_file.dart
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ class Save {
children = [
XmlElement(XmlName('v'), [], [XmlText(value.value ? '1' : '0')]),
];
case ImageCellValue():
// TODO: Handle this case.
throw "Image writing not implemented";
}

return XmlElement(XmlName('c'), attributes, children);
Expand Down Expand Up @@ -487,7 +490,7 @@ class Save {
}
return MapEntry<int, CustomNumFormat>(e.key, format);
})
.whereNotNull()
.nonNulls
.sorted((a, b) => a.key.compareTo(b.key));

if (customNumberFormats.isNotEmpty) {
Expand Down
20 changes: 20 additions & 0 deletions lib/src/sheet/data_model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,25 @@ class IntCellValue extends CellValue {
}
}

class ImageCellValue extends CellValue {
final Uint8List value;

const ImageCellValue(this.value);

@override
String toString() {
return value.toString();
}

@override
int get hashCode => Object.hash(runtimeType, value);

@override
operator ==(Object other) {
return other is IntCellValue && other.value == value;
}
}

class DoubleCellValue extends CellValue {
final double value;

Expand Down Expand Up @@ -214,6 +233,7 @@ class TextCellValue extends CellValue {
final TextSpan value;

TextCellValue(String text) : value = TextSpan(text: text);

TextCellValue.span(this.value);

@override
Expand Down
6 changes: 6 additions & 0 deletions lib/src/utilities/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ const _relationshipsStyles =
const _relationshipsWorksheet =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet";

const _relationshipsImage =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";

const _relationshipsDrawing =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing";

const _relationshipsSharedStrings =
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings";

Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ dependencies:
xml: ">=5.0.0 <7.0.0"
collection: ^1.15.0
equatable: ^2.0.0
path: ^1.9.1

dev_dependencies:
test: ^1.23.0
Expand Down
Loading