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

feat: #304 - new method setProductImageAngle #309

Merged
merged 3 commits into from Dec 9, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
85 changes: 83 additions & 2 deletions lib/model/ProductImage.dart
Expand Up @@ -61,15 +61,96 @@ 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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a comment here that it only allow 0, 90, 170 and 270

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,
this.coordinatesImageSize,
this.x1,
this.y1,
this.x2,
this.y2,
});

final ImageField field;
final ImageSize? size;
final OpenFoodFactsLanguage? language;
String? url;

/// Revision number
int? rev;

/// Uploaded image id (probably an `int`)
String? imgid;

/// Image angle, compared to the uploaded image
ImageAngle? angle;

/// On what size are the coordinates ([x1], ...)computed? 'full' or '400'
String? coordinatesImageSize;

/// Crop coordinate x1, compared to the uploaded image
int? x1;

/// Crop coordinate y1, compared to the uploaded image
int? y1;

/// Crop coordinate x2, compared to the uploaded image
int? x2;

/// Crop coordinate y2, compared to the uploaded image
int? y2;

@override
String toString() =>
'ProductImage('
'${field.value}' +
(size == null ? '' : ',size=${size.value}]') +
(language == null ? '' : ',language=${language.code}') +
(angle == null ? '' : ',angle=${angle!.degreesClockwise}') +
(url == null ? '' : ',url=$url') +
(imgid == null ? '' : ',imgid=$imgid') +
(rev == null ? '' : ',rev=$rev') +
(coordinatesImageSize == null
? ''
: ',coordinatesImageSize=$coordinatesImageSize') +
(x1 == null ? '' : ',x1=$x1') +
(y1 == null ? '' : ',y1=$y1') +
(x2 == null ? '' : ',x2=$x2') +
(y2 == null ? '' : ',y2=$y2') +
')';
}
117 changes: 117 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 All @@ -18,6 +19,7 @@ import 'package:openfoodfacts/model/TaxonomyLabel.dart';
import 'package:openfoodfacts/model/TaxonomyLanguage.dart';
import 'package:openfoodfacts/utils/AbstractQueryConfiguration.dart';
import 'package:openfoodfacts/utils/CountryHelper.dart';
import 'package:openfoodfacts/utils/ImageHelper.dart';
import 'package:openfoodfacts/utils/OcrField.dart';
import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart';
import 'package:openfoodfacts/utils/PnnsGroupQueryConfiguration.dart';
Expand Down Expand Up @@ -907,4 +909,119 @@ class OpenFoodAPIClient {
final json = jsonDecode(response.body);
return OrderedNutrients.fromJson(json);
}

/// Rotates a product image from an already uploaded image.
///
/// "I want, for this [barcode], this [imageField] and this [language],
/// the image to be computed from the already uploaded image
/// referenced by [imgid], with a rotation of [angle].
///
/// Returns the URL to the "display" picture after the operation.
/// Returns null if KO, but would probably throw an exception instead.
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,
},
);

/// Crops a product image from an already uploaded image.
///
/// "I want, for this [barcode], this [imageField] and this [language],
/// the image to be computed from the already uploaded image
/// referenced by [imgid], with a possible rotation of [angle] and then
/// a cropping on rectangle ([x1],[y1],[x2],[y2]), those coordinates
/// being taken from the uploaded image size.
///
/// Returns the URL to the "display" picture after the operation.
/// Returns null if KO, but would probably throw an exception instead.
static Future<String?> setProductImageCrop({
required final String barcode,
required final ImageField imageField,
required final OpenFoodFactsLanguage language,
required final String imgid,
required final int x1,
required final int y1,
required final int x2,
required final int y2,
final ImageAngle angle = ImageAngle.NOON,
final QueryType? queryType,
}) async =>
await _callProductImageCrop(
barcode: barcode,
imageField: imageField,
language: language,
imgid: imgid,
extraParameters: <String, String>{
'x1': x1.toString(),
'y1': y1.toString(),
'x2': x2.toString(),
'y2': y2.toString(),
'angle': angle.degreesClockwise,
'coordinates_image_size': 'full',
},
);

/// Calls `cgi/product_image_crop.pl` on a [ProductImage].
///
/// Returns the URL to the "display" picture after the operation.
/// Returns null if KO, but would probably throw an exception instead.
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) {
throw Exception(
'Bad response (${response.statusCode}): ${response.body}');
}
final Map<String, dynamic> json =
jsonDecode(response.body) as Map<String, dynamic>;
final String status = json['status'];
if (status != 'status ok') {
throw Exception('Status not ok ($status)');
}
final String imagefield = json['imagefield'];
if (imagefield != id) {
throw Exception(
'Different imagefield: expected "$id", actual "$imageField"');
}
final Map<String, dynamic> images = json['image'];
final String? filename = images['display_url'];
if (filename == null) {
return null;
}
return ImageHelper.getProductImageRootUrl(barcode, queryType: queryType) +
'/' +
filename;
}
}
71 changes: 48 additions & 23 deletions lib/utils/ImageHelper.dart
@@ -1,4 +1,5 @@
import 'package:image/image.dart';
import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart';
import 'package:openfoodfacts/utils/QueryType.dart';
import '../model/ProductImage.dart';
import 'LanguageHelper.dart';
Expand Down Expand Up @@ -26,36 +27,60 @@ class ImageHelper {
}
}

/// Returns the image url
/// E.g. https://static.openfoodfacts.org/images/products/359/671/046/2858/front_fr.4.100.jpg"
static String? buildUrl(String? barcode, ProductImage image,
{QueryType queryType = QueryType.PROD}) {
if (barcode == null) {
return null;
}
String barcodeUrl = barcode;
/// Returns the product image full url, or null if [barcode] is null
///
/// E.g. "https://static.openfoodfacts.org/images/products/359/671/046/2858/front_fr.4.100.jpg"
static String? buildUrl(
final String? barcode,
final ProductImage image, {
final QueryType? queryType,
}) =>
barcode == null
? null
: getProductImageRootUrl(barcode, queryType: queryType) +
'/' +
image.field.value +
'_' +
image.language.code +
'.' +
image.rev.toString() +
'.' +
image.size.toNumber() +
'.jpg';
Copy link
Member

@M123-dev M123-dev Dec 9, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does the server allow other types then .jpg


/// Returns the product image filename
///
/// E.g. "front_fr.4.100.jpg"
static String getProductImageFilename(final ProductImage image) =>
image.field.value +
'_' +
image.language.code +
'.' +
image.rev.toString() +
'.' +
image.size.toNumber() +
'.jpg';

/// Returns the web folder of the product images (without trailing '/')
///
/// E.g. "https://static.openfoodfacts.org/images/products/359/671/046/2858"
static String getProductImageRootUrl(
final String barcode, {
final QueryType? queryType,
}) {
final String barcodeUrl;
if (barcode.length >= 9) {
var p1 = barcode.substring(0, 3);
var p2 = barcode.substring(3, 6);
var p3 = barcode.substring(6, 9);
var p4 = barcode.length >= 10 ? barcode.substring(9) : '';

barcodeUrl = p1 + '/' + p2 + '/' + p3 + '/' + p4;
} else {
barcodeUrl = barcode;
}

String urlHelper = barcodeUrl +
'/' +
image.field.value +
'_' +
image.language.code +
'.' +
image.rev.toString() +
'.' +
image.size.toNumber() +
'.jpg';

return queryType == QueryType.PROD
? IMAGE_PROD_URL_BASE + urlHelper
: IMAGE_TEST_URL_BASE + urlHelper;
return OpenFoodAPIConfiguration.getQueryType(queryType) == QueryType.PROD
? IMAGE_PROD_URL_BASE + barcodeUrl
: IMAGE_TEST_URL_BASE + barcodeUrl;
}
}
32 changes: 26 additions & 6 deletions lib/utils/JsonHelper.dart
Expand Up @@ -72,14 +72,23 @@ class JsonHelper {
for (var field in ImageField.values) {
for (OpenFoodFactsLanguage lang in OpenFoodFactsLanguage.values) {
// get the field object e.g. front_en
String fieldName = field.value + '_' + lang.code;
final String fieldName = field.value + '_' + lang.code;
if (json[fieldName] == null) continue;

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

// get the rev object
var rev = JsonObject.parseInt(fieldObject['rev']);
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'];
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>?;
Expand All @@ -91,8 +100,19 @@ 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,
coordinatesImageSize: coordinatesImageSize,
x1: x1,
y1: y1,
x2: x2,
y2: y2,
);
imageList.add(image);
}
}
Expand Down
3 changes: 1 addition & 2 deletions lib/utils/ProductHelper.dart
@@ -1,6 +1,5 @@
import 'package:openfoodfacts/utils/ImageHelper.dart';
import 'package:openfoodfacts/utils/LanguageHelper.dart';
import 'package:openfoodfacts/utils/OpenFoodAPIConfiguration.dart';
import 'package:openfoodfacts/utils/QueryType.dart';

import '../model/Product.dart';
Expand Down Expand Up @@ -36,7 +35,7 @@ class ProductHelper {
image.url = ImageHelper.buildUrl(
product.barcode,
image,
queryType: OpenFoodAPIConfiguration.getQueryType(queryType),
queryType: queryType,
);
}
}
Expand Down