Skip to content

Commit

Permalink
feat: openfoodfacts#304 - new method setProductImageAngle
Browse files Browse the repository at this point in the history
Deleted file:
* `corn_da.jpg`

Impacted files:
* `api_addProductImage_test.dart`: added a test for `OpenFoodAPIClient.setProductImageAngle`; removed a duplicate test
* `JsonHelper.dart`: decoded new fields `imgid` and `angle` for product `'images`''
* `openfoodfacts.dart`: new method `setProductImageAngle`
* `ProductImage.dart`: added `enum ImageAngle`; added `imgid` and `angle` to `ProductImage`
* `UriHelper.dart`: unrelated refactoring
  • Loading branch information
monsieurtanuki committed Dec 5, 2021
1 parent 85169f0 commit 10177f6
Show file tree
Hide file tree
Showing 6 changed files with 277 additions and 56 deletions.
51 changes: 49 additions & 2 deletions lib/model/ProductImage.dart
Expand Up @@ -61,15 +61,62 @@ extension ImageSizeExtension on ImageSize? {
);
}

enum ImageAngle {
NOON,
THREE_O_CLOCK,
SIX_O_CLOCK,
NINE_O_CLOCK,
}

extension ImageAngleExtension on ImageAngle {
static const Map<ImageAngle, int> _DEGREES_CLOCKWISE = {
ImageAngle.NOON: 0,
ImageAngle.THREE_O_CLOCK: 90,
ImageAngle.SIX_O_CLOCK: 180,
ImageAngle.NINE_O_CLOCK: 270,
};

String get degreesClockwise => _DEGREES_CLOCKWISE[this]?.toString() ?? '0';

static ImageAngle? fromInt(final int? angle) {
for (MapEntry<ImageAngle, int> entry in _DEGREES_CLOCKWISE.entries) {
if (entry.value == angle) {
return entry.key;
}
}
return null;
}
}

/// The url to a specific product image.
/// Categorized by content type, size and language
class ProductImage {
ProductImage(
{required this.field, this.size, this.language, this.url, this.rev});
ProductImage({
required this.field,
this.size,
this.language,
this.url,
this.rev,
this.imgid,
this.angle,
});

final ImageField field;
final ImageSize? size;
final OpenFoodFactsLanguage? language;
String? url;
int? rev;
String? imgid;
ImageAngle? angle;

@override
String toString() => 'ProductImage('
'${field.value}'
',size=${size?.value}]'
',language=${language?.code}'
',angle=${angle?.degreesClockwise}'
',url=$url'
',imgid=$imgid'
',rev=$rev'
')';
}
67 changes: 67 additions & 0 deletions lib/openfoodfacts.dart
Expand Up @@ -9,6 +9,7 @@ import 'package:openfoodfacts/interface/JsonObject.dart';
import 'package:openfoodfacts/model/KnowledgePanels.dart';
import 'package:openfoodfacts/model/OcrIngredientsResult.dart';
import 'package:openfoodfacts/model/OrderedNutrients.dart';
import 'package:openfoodfacts/model/ProductImage.dart';
import 'package:openfoodfacts/model/TaxonomyAdditive.dart';
import 'package:openfoodfacts/model/TaxonomyAllergen.dart';
import 'package:openfoodfacts/model/TaxonomyCategory.dart';
Expand Down Expand Up @@ -907,4 +908,70 @@ class OpenFoodAPIClient {
final json = jsonDecode(response.body);
return OrderedNutrients.fromJson(json);
}

/// Sets the angle of a product image
///
/// Returns the filename of the "display" picture after the operation,
/// or null if KO.
/// The parameter is an angle, not a rotation.
/// No extra rotation if the picture angle is already set to the same value.
static Future<String?> setProductImageAngle({
required final String barcode,
required final ImageField imageField,
required final OpenFoodFactsLanguage language,
required final String imgid,
required final ImageAngle angle,
final QueryType? queryType,
}) async =>
await _callProductImageCrop(
barcode: barcode,
imageField: imageField,
language: language,
imgid: imgid,
extraParameters: <String, String>{
'angle': angle.degreesClockwise,
},
);

/// Calls `cgi/product_image_crop.pl` on a [ProductImage].
///
/// Returns the filename of the "display" picture after the operation,
/// or null if KO.
static Future<String?> _callProductImageCrop({
required final String barcode,
required final ImageField imageField,
required final OpenFoodFactsLanguage language,
required final String imgid,
required final Map<String, String> extraParameters,
final QueryType? queryType,
}) async {
final String id = '${imageField.value}_${language.code}';
final Map<String, String> queryParameters = <String, String>{
'code': barcode,
'id': id,
'imgid': imgid,
};
queryParameters.addAll(extraParameters);
final Uri uri = UriHelper.getUri(
path: 'cgi/product_image_crop.pl',
queryType: queryType,
queryParameters: queryParameters,
);

final Response response = await HttpHelper()
.doGetRequest(uri, userAgent: OpenFoodAPIConfiguration.userAgent);
if (response.statusCode != 200) {
return null;
}
final Map<String, dynamic> json =
jsonDecode(response.body) as Map<String, dynamic>;
if (json['status'] != 'status ok') {
return null;
}
if (json['imagefield'] != id) {
return null;
}
final Map<String, dynamic> images = json['image'];
return images['display_url'];
}
}
18 changes: 16 additions & 2 deletions lib/utils/JsonHelper.dart
Expand Up @@ -81,6 +81,14 @@ class JsonHelper {
// get the rev object
var rev = JsonObject.parseInt(fieldObject['rev']);

// get the imgid
final String imgid = fieldObject['imgid'].toString();

// get the angle
final ImageAngle? angle = ImageAngleExtension.fromInt(
JsonObject.parseInt(fieldObject['angle']),
);

// get the sizes object
var sizesObject = fieldObject['sizes'] as Map<String, dynamic>?;
if (sizesObject == null) continue;
Expand All @@ -91,8 +99,14 @@ class JsonHelper {
var numberObject = sizesObject[number] as Map<String, dynamic>?;
if (numberObject == null) continue;

var image =
ProductImage(field: field, size: size, language: lang, rev: rev);
var image = ProductImage(
field: field,
size: size,
language: lang,
rev: rev,
imgid: imgid,
angle: angle,
);
imageList.add(image);
}
}
Expand Down
50 changes: 24 additions & 26 deletions lib/utils/UriHelper.dart
Expand Up @@ -10,35 +10,33 @@ class UriHelper {

///Returns a OFF uri with the in the [OpenFoodAPIConfiguration] specified settings
static Uri getUri({
String? path,
Map<String, dynamic>? queryParameters,
QueryType? queryType,
}) {
return Uri(
scheme: OpenFoodAPIConfiguration.uriScheme,
host: OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? OpenFoodAPIConfiguration.uriProdHost
: OpenFoodAPIConfiguration.uriTestHost,
path: path,
queryParameters: queryParameters,
);
}
required final String path,
final Map<String, dynamic>? queryParameters,
final QueryType? queryType,
}) =>
Uri(
scheme: OpenFoodAPIConfiguration.uriScheme,
host: OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? OpenFoodAPIConfiguration.uriProdHost
: OpenFoodAPIConfiguration.uriTestHost,
path: path,
queryParameters: queryParameters,
);

///Returns a OFF-Robotoff uri with the in the [OpenFoodAPIConfiguration] specified settings
static Uri getRobotoffUri({
String? path,
Map<String, dynamic>? queryParameters,
QueryType? queryType,
}) {
return Uri(
scheme: OpenFoodAPIConfiguration.uriScheme,
host: OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? OpenFoodAPIConfiguration.uriProdHostRobotoff
: OpenFoodAPIConfiguration.uriTestHostRobotoff,
path: path,
queryParameters: queryParameters,
);
}
required final String path,
final Map<String, dynamic>? queryParameters,
final QueryType? queryType,
}) =>
Uri(
scheme: OpenFoodAPIConfiguration.uriScheme,
host: OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? OpenFoodAPIConfiguration.uriProdHostRobotoff
: OpenFoodAPIConfiguration.uriTestHostRobotoff,
path: path,
queryParameters: queryParameters,
);

/// Replaces the subdomain of an URI with specific country and language
///
Expand Down

0 comments on commit 10177f6

Please sign in to comment.