Skip to content

Commit

Permalink
fix: 691 - implemented "imagesToJson" (#693)
Browse files Browse the repository at this point in the history
* fix: 691 - implemented "imagesToJson"

New file:
* `api_json_to_from_test.dart`: unit tests about to/from json conversions

Impacted files:
* `api_search_products_test.dart`: unrelated fix
* `json_helper.dart`: implemented method `imagesToJson`; refactored method `imagesFromJson`
* `product_images.dart`: added fields width and height; added `hashcode` and `==` operator

* fix: 691 - unrelated fix of 692

Impacted file:
* `product.dart`: removed deprecated json annotation.
  • Loading branch information
monsieurtanuki committed Jan 27, 2023
1 parent 2d3f76f commit 316b19d
Show file tree
Hide file tree
Showing 5 changed files with 239 additions and 52 deletions.
12 changes: 7 additions & 5 deletions lib/src/model/product.dart
Original file line number Diff line number Diff line change
Expand Up @@ -224,16 +224,18 @@ class Product extends JsonObject {
toJson: IngredientsAnalysisTags.toJson)
IngredientsAnalysisTags? ingredientsAnalysisTags;

/// When no nutrition data is true, nutriments are always null
/// When no nutrition data is true, nutriments are always null.
///
/// This logic is handled by the getters/setters of [noNutritionData] and
/// [nutriments]
@JsonKey(ignore: true)
/// [nutriments].
/// This field is therefore not populated directly by json.
bool? _noNutritionData;

/// When nutriments are not null, no nutrition data can't be true
/// When nutriments are not null, no nutrition data can't be true.
///
/// This logic is handled by the getters/setters of [noNutritionData] and
/// [nutriments]
@JsonKey(ignore: true)
/// This field is therefore not populated directly by json.
Nutriments? _nutriments;

@JsonKey(
Expand Down
39 changes: 39 additions & 0 deletions lib/src/model/product_image.dart
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ class ProductImage {
this.y1,
this.x2,
this.y2,
this.width,
this.height,
});

final ImageField field;
Expand Down Expand Up @@ -165,6 +167,12 @@ class ProductImage {
/// Crop coordinate y2, compared to the uploaded image
int? y2;

/// Image width.
int? width;

/// Image height.
int? height;

@override
String toString() => 'ProductImage('
'${field.offTag}'
Expand All @@ -179,5 +187,36 @@ class ProductImage {
'${y1 == null ? '' : ',y1=$y1'}'
'${x2 == null ? '' : ',x2=$x2'}'
'${y2 == null ? '' : ',y2=$y2'}'
'${width == null ? '' : ',width=$width'}'
'${height == null ? '' : ',height=$height'}'
')';

@override
int get hashCode =>
'${field.offTag}_${language?.code}_${size?.offTag}'.hashCode;

@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is ProductImage &&
other.field == field &&
other.size == size &&
other.language == language &&
other.url == url &&
other.rev == rev &&
other.imgid == imgid &&
other.angle == angle &&
other.coordinatesImageSize == coordinatesImageSize &&
other.x1 == x1 &&
other.y1 == y1 &&
other.x2 == x2 &&
other.y2 == y2 &&
other.width == width &&
other.height == height;
}
}
193 changes: 146 additions & 47 deletions lib/src/utils/json_helper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -75,67 +75,166 @@ class JsonHelper {
return result;
}

/// Returns [ProductImage]s from a JSON map for "Images"
/// Returns [ProductImage]s from a JSON map for "Images".
///
/// For historical reasons we keep only the 4 main images here, on all sizes
/// and languages.
static List<ProductImage>? imagesFromJson(Map? json) {
if (json == null) return null;

var imageList = <ProductImage>[];

for (var field in ImageField.values) {
for (OpenFoodFactsLanguage lang in OpenFoodFactsLanguage.values) {
// get the field object e.g. front_en
final String fieldName = '${field.offTag}_${lang.offTag}';
if (json[fieldName] == null) continue;

final fieldObject = json[fieldName] as Map<String, dynamic>?;
if (fieldObject == null) continue;

final rev = JsonObject.parseInt(fieldObject['rev']);
final String imgid = fieldObject['imgid'].toString();
final ImageAngle? angle = ImageAngleExtension.fromInt(
JsonObject.parseInt(fieldObject['angle']),
);
final String? coordinatesImageSize =
fieldObject['coordinates_image_size']?.toString();
final int? x1 = JsonObject.parseInt(fieldObject['x1']);
final int? y1 = JsonObject.parseInt(fieldObject['y1']);
final int? x2 = JsonObject.parseInt(fieldObject['x2']);
final int? y2 = JsonObject.parseInt(fieldObject['y2']);

// get the sizes object
var sizesObject = fieldObject['sizes'] as Map<String, dynamic>?;
if (sizesObject == null) continue;

// get each number object (e.g. 200)
for (var size in ImageSize.values) {
var number = size.number;
var numberObject = sizesObject[number] as Map<String, dynamic>?;
if (numberObject == null) continue;
for (final String key in json.keys) {
final int? imageId = int.tryParse(key);
if (imageId != null) {
// we can expect integer (imageIds) and String (field + language)
// here we ignore imageIds.
continue;
}
final List<String> values = key.split('_');
if (values.length != 2) {
// we expect field + '_' + language
continue;
}
final String fieldString = values[0];
final ImageField? field = ImageField.fromOffTag(fieldString);
if (field == null) {
continue;
}
final String languageString = values[1];
final OpenFoodFactsLanguage? lang =
OpenFoodFactsLanguage.fromOffTag(languageString);
if (lang == null) {
continue;
}

var image = ProductImage(
field: field,
size: size,
language: lang,
rev: rev,
imgid: imgid,
angle: angle,
coordinatesImageSize: coordinatesImageSize,
x1: x1,
y1: y1,
x2: x2,
y2: y2,
);
imageList.add(image);
final Map<String, dynamic> fieldObject = json[key];

// get the sizes object
var sizesObject = fieldObject['sizes'] as Map<String, dynamic>?;
if (sizesObject == null) {
continue;
}

final rev = JsonObject.parseInt(fieldObject['rev']);
final String imgid = fieldObject['imgid'].toString();
final ImageAngle? angle = ImageAngleExtension.fromInt(
JsonObject.parseInt(fieldObject['angle']),
);
final String? coordinatesImageSize =
fieldObject['coordinates_image_size']?.toString();
final int? x1 = JsonObject.parseInt(fieldObject['x1']);
final int? y1 = JsonObject.parseInt(fieldObject['y1']);
final int? x2 = JsonObject.parseInt(fieldObject['x2']);
final int? y2 = JsonObject.parseInt(fieldObject['y2']);

// get each number object (e.g. 200)
for (var size in ImageSize.values) {
var number = size.number;
var numberObject = sizesObject[number] as Map<String, dynamic>?;
if (numberObject == null) {
continue;
}
final int? width = JsonObject.parseInt(numberObject['w']);
final int? height = JsonObject.parseInt(numberObject['h']);

// TODO(monsieurtanuki): add field "url"?
var image = ProductImage(
field: field,
size: size,
language: lang,
rev: rev,
imgid: imgid,
angle: angle,
coordinatesImageSize: coordinatesImageSize,
x1: x1,
y1: y1,
x2: x2,
y2: y2,
width: width,
height: height,
);
imageList.add(image);
}
}

return imageList;
}

// TODO(monsieurtanuki): not implemented and needed, yet.
static Map<String, dynamic> imagesToJson(List<ProductImage>? images) {
return {};
final Map<String, dynamic> result = <String, dynamic>{};
if (images == null || images.isEmpty) {
return result;
}
// grouped by "front_fr"-like keys
final Map<String, List<ProductImage>> sorted =
<String, List<ProductImage>>{};
for (final ProductImage productImage in images) {
if (productImage.language == null) {
continue;
}
final String key =
'${productImage.field.offTag}_${productImage.language!.offTag}';
List<ProductImage>? items = sorted[key];
if (items == null) {
items = <ProductImage>[];
sorted[key] = items;
}
items.add(productImage);
}
for (final MapEntry<String, List<ProductImage>> entry in sorted.entries) {
final String key = entry.key;
final List<ProductImage> list = entry.value;
if (list.isEmpty) {
// very unlikely
continue;
}
final Map<String, dynamic> item = <String, dynamic>{};
item['sizes'] = <String, Map<String, int>>{};
bool first = true;
for (final ProductImage productImage in list) {
if (productImage.size == null) {
continue;
}
final Map<String, int> size = <String, int>{};
if (productImage.width != null) {
size['w'] = productImage.width!;
}
if (productImage.height != null) {
size['h'] = productImage.height!;
}
item['sizes']![productImage.size!.number] = size;
if (first) {
first = false;
if (productImage.rev != null) {
item['rev'] = productImage.rev.toString();
}
if (productImage.imgid != null) {
item['imgid'] = productImage.imgid;
}
if (productImage.angle != null) {
item['angle'] = productImage.angle!.degree.toString();
}
if (productImage.coordinatesImageSize != null) {
item['coordinates_image_size'] = productImage.coordinatesImageSize;
}
if (productImage.x1 != null) {
item['x1'] = productImage.x1;
}
if (productImage.y1 != null) {
item['y1'] = productImage.y1;
}
if (productImage.x2 != null) {
item['x2'] = productImage.x2;
}
if (productImage.y2 != null) {
item['y2'] = productImage.y2;
}
}
}
result[key] = item;
}
return result;
}

/// Returns a double from a JSON-encoded int or double
Expand Down
43 changes: 43 additions & 0 deletions test/api_json_to_from_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import 'package:openfoodfacts/openfoodfacts.dart';
import 'package:test/test.dart';

void main() {
const BARCODE_DANISH_BUTTER_COOKIES = '5701184005007';

OpenFoodAPIConfiguration.globalQueryType = QueryType.PROD;

group('$OpenFoodAPIClient json to/from conversions', () {
test('images', () async {
final ProductResultV3 productResult =
await OpenFoodAPIClient.getProductV3(
ProductQueryConfiguration(
BARCODE_DANISH_BUTTER_COOKIES,
fields: [ProductField.IMAGES],
version: ProductQueryVersion.v3,
),
);
expect(productResult.product, isNotNull);

final List<ProductImage>? images = productResult.product!.images;
expect(images, isNotNull);
expect(images, isNotEmpty);

final List<ProductImage>? imagesBackAndForth = JsonHelper.imagesFromJson(
JsonHelper.imagesToJson(images),
);
expect(imagesBackAndForth, isNotNull);
expect(imagesBackAndForth, isNotEmpty);

expect(imagesBackAndForth!.length, images!.length);
for (final ProductImage productImage1 in images) {
int count = 0;
for (final ProductImage productImage2 in imagesBackAndForth) {
if (productImage1 == productImage2) {
count++;
}
}
expect(count, 1);
}
});
});
}
4 changes: 4 additions & 0 deletions test/api_search_products_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1089,9 +1089,13 @@ void main() {
final Random random = Random();
final bool completed1 = random.nextBool();
final bool completed2 = random.nextBool();
// TODO(monsieurtanuki): sometimes fails because of bad luck
// by the time the second count is retrieved, the first count changed too,
// if we are unlucky.
await checkExpectations(state1, completed1, state2, completed2);
});
},
skip: 'Sometimes fails because of bad-luck',
timeout: Timeout(
// some tests can be slow here
Duration(seconds: 300),
Expand Down

0 comments on commit 316b19d

Please sign in to comment.