Skip to content

Commit

Permalink
feat: simplified access to raw product images (openfoodfacts#839)
Browse files Browse the repository at this point in the history
* feat: simplified access to raw product images

Impacted files:
* `api_get_product_image_ids_test.dart`: used new method and extended tests to all/main/raw images
* `api_get_product_test.dart`: minor refactoring
* `api_json_to_from_test.dart`: extended tests to all/main/raw images
* `image_helper.dart`: minor refactoring
* `json_helper.dart`: integrated "raw" images in addition to "main" images
* `open_food_api_client.dart`: deprecated method `getProductImageIds`
* `product.dart`: integrated "raw" images in addition to "main" images
* `product.g.dart`: generated
* `product_helper.dart`: minor refactoring
* `product_image.dart`: now can include "raw" images

* test fix

* Edits after review
  • Loading branch information
monsieurtanuki authored Dec 24, 2023
1 parent 0482f63 commit 3350251
Show file tree
Hide file tree
Showing 11 changed files with 341 additions and 159 deletions.
30 changes: 28 additions & 2 deletions lib/src/model/product.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,36 @@ class Product extends JsonObject {
@JsonKey(
name: 'images',
includeIfNull: false,
fromJson: JsonHelper.imagesFromJson,
toJson: JsonHelper.imagesToJson)
fromJson: JsonHelper.allImagesFromJson,
toJson: JsonHelper.allImagesToJson)

/// All images in bulk. Will include "main" images and "raw" images.
///
/// See also [getRawImages] and [getMainImages].
List<ProductImage>? images;

/// "Raw" (uploaded) images: for example "picture 12" resized to "400" size.
List<ProductImage>? getRawImages() => _getImageSubset(false);

/// "Main" images: the selected images for certain criteria.
///
/// For example the "front" picture in "French"
/// Images may be returned in multiple sizes
List<ProductImage>? getMainImages() => _getImageSubset(true);

List<ProductImage>? _getImageSubset(final bool isMain) {
if (images == null) {
return null;
}
final List<ProductImage> result = <ProductImage>[];
for (final ProductImage productImage in images!) {
if (productImage.isMain == isMain) {
result.add(productImage);
}
}
return result;
}

@JsonKey(
name: 'ingredients',
includeIfNull: false,
Expand Down
4 changes: 2 additions & 2 deletions lib/src/model/product.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

93 changes: 85 additions & 8 deletions lib/src/model/product_image.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'off_tagged.dart';
import '../utils/language_helper.dart';
import '../utils/open_food_api_configuration.dart';
import '../utils/uri_helper.dart';

enum ImageField implements OffTagged {
FRONT(offTag: 'front'),
Expand Down Expand Up @@ -95,13 +97,20 @@ extension ImageAngleExtension on ImageAngle {
}
}

/// The url to a specific product image.
/// Categorized by content type, size and language
/// Product image. Can be "main" (e.g. "front_fr") or "raw" (e.g. "picture #9").
///
/// For "main" images the key is field + language + size.
/// For "raw" images the key is the imgid + size.
///
/// We have limited data for "raw" images, like "this is the nth image for this
/// product".
/// We have more data for "main" images, like "we built this image from that raw
/// image with this crop parameters and angle".
class ProductImage {
ProductImage({
required this.field,
required ImageField this.field,
this.size,
this.language,
required OpenFoodFactsLanguage this.language,
this.url,
this.rev,
this.imgid,
Expand All @@ -115,7 +124,16 @@ class ProductImage {
this.height,
});

final ImageField field;
ProductImage.raw({
this.size,
this.url,
required String this.imgid,
this.width,
this.height,
}) : language = null,
field = null;

final ImageField? field;
final ImageSize? size;
final OpenFoodFactsLanguage? language;
String? url;
Expand Down Expand Up @@ -150,9 +168,66 @@ class ProductImage {
/// Image height.
int? height;

bool get isMain => field != null && language != null;

/// Returns the url to display this image.
///
/// cf. https://github.com/openfoodfacts/smooth-app/issues/3065
String getUrl(
final String barcode, {
final ImageSize? imageSize,
final UriProductHelper uriHelper = uriHelperFoodProd,
}) =>
'${uriHelper.getProductImageRootUrl(barcode)}'
'/'
'${getUrlFilename(imageSize: imageSize)}';

/// Returns just the filename of the url to display this image.
///
/// See also [getUrl].
String getUrlFilename({
final ImageSize? imageSize,
}) {
final ImageSize bestImageSize = imageSize ?? size ?? ImageSize.UNKNOWN;
return isMain
? _getMainUrlFilename(bestImageSize)
: _getRawUrlFilename(bestImageSize);
}

/// Returns just the filename of the url to display this "main" image.
///
/// By default uses the own [image]'s size field.
/// E.g. "front_fr.4.100.jpg"
/// cf. https://github.com/openfoodfacts/smooth-app/issues/3065
String _getMainUrlFilename(final ImageSize imageSize) =>
'${field!.offTag}_${language.code}'
'.$rev'
'.${imageSize.number}'
'.jpg';

/// Returns just the filename of the url to display this "raw" image.
///
/// By default uses the own [image]'s size field.
/// E.g. "7.100.jpg"
String _getRawUrlFilename(final ImageSize imageSize) {
switch (imageSize) {
case ImageSize.THUMB:
case ImageSize.DISPLAY:
// adapted size
return '$imgid.${imageSize.number}.jpg';
case ImageSize.SMALL:
// not available, we take the best other choice instead
return '$imgid.${ImageSize.DISPLAY.number}.jpg';
case ImageSize.ORIGINAL:
case ImageSize.UNKNOWN:
// full size
return '$imgid.jpg';
}
}

@override
String toString() => 'ProductImage('
'${field.offTag}'
'${field == null ? '' : 'field=${field!.offTag}'}'
'${size == null ? '' : ',size=${size!.offTag}'}'
'${language == null ? '' : ',language=${language.code}'}'
'${angle == null ? '' : ',angle=${angle!.degreesClockwise}'}'
Expand All @@ -168,9 +243,11 @@ class ProductImage {
'${height == null ? '' : ',height=$height'}'
')';

String get _key =>
'${field?.offTag}_${language?.code}_${size?.offTag}_$imgid';

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

@override
bool operator ==(Object other) {
Expand Down
2 changes: 2 additions & 0 deletions lib/src/open_food_api_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ class OpenFoodAPIClient {
///
/// To be used in combination with [ImageHelper.getUploadedImageUrl].
/// Does not depend on language or country.
// TODO: deprecated from 2023-11-25; remove when old enough
@Deprecated('Use product field "images" instead')
static Future<List<int>> getProductImageIds(
final String barcode, {
final User? user,
Expand Down
48 changes: 19 additions & 29 deletions lib/src/utils/image_helper.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'language_helper.dart';
import 'open_food_api_configuration.dart';
import 'uri_helper.dart';
import '../model/product_image.dart';
Expand All @@ -14,65 +13,56 @@ class ImageHelper {
/// Returns the [image] full url - for a specific [imageSize] if needed.
///
/// E.g. "https://images.openfoodfacts.org/images/products/359/671/046/2858/front_fr.4.100.jpg"
// TODO: deprecated from 2023-11-25; remove when old enough
@Deprecated('Use ProductImage.getUrl instead')
static String getLocalizedProductImageUrl(
final String barcode,
final ProductImage image, {
final ImageSize? imageSize,
final UriProductHelper uriHelper = uriHelperFoodProd,
}) =>
'${uriHelper.getProductImageRootUrl(barcode)}'
'/'
'${getProductImageFilename(
image,
imageSize: imageSize,
)}';
image.getUrl(barcode, uriHelper: uriHelper, imageSize: imageSize);

/// Returns the [image] full url for an uploaded ("raw") image.
///
/// E.g. "https://images.openfoodfacts.org/images/products/359/671/046/2858/1.400.jpg"
// TODO: deprecated from 2023-11-25; remove when old enough
@Deprecated('Use ProductImage.getUrl instead')
static String getUploadedImageUrl(
final String barcode,
final int imageId,
final ImageSize imageSize, {
final UriProductHelper uriHelper = uriHelperFoodProd,
}) =>
'${uriHelper.getProductImageRootUrl(barcode)}'
'/'
'${getUploadedImageFilename(imageId, imageSize)}';
ProductImage.raw(imgid: imageId.toString(), size: imageSize).getUrl(
barcode,
uriHelper: uriHelper,
);

/// Returns the [image] filename - for a specific [imageSize] if needed.
///
/// By default uses the own [image]'s size field.
/// E.g. "front_fr.4.100.jpg"
/// cf. https://github.com/openfoodfacts/smooth-app/issues/3065
// TODO: deprecated from 2023-11-25; remove when old enough
@Deprecated('Use ProductImage.getUrlFilename instead')
static String getProductImageFilename(
final ProductImage image, {
final ImageSize? imageSize,
}) =>
'${image.field.offTag}_${image.language.code}'
'.${image.rev}'
'.${((imageSize ?? image.size) ?? ImageSize.UNKNOWN).number}'
'.jpg';
image.getUrlFilename(imageSize: imageSize);

/// Returns the filename of an uploaded image.
///
/// cf. https://github.com/openfoodfacts/smooth-app/issues/3065
// TODO: deprecated from 2023-11-25; remove when old enough
@Deprecated('Use ProductImage.getUrlFilename instead')
static String getUploadedImageFilename(
final int imageId,
final ImageSize imageSize,
) {
switch (imageSize) {
case ImageSize.THUMB:
case ImageSize.DISPLAY:
// adapted size
return '$imageId.${imageSize.number}.jpg';
case ImageSize.SMALL:
// not available, we take the best other choice instead
return '$imageId.${ImageSize.DISPLAY.number}.jpg';
case ImageSize.ORIGINAL:
case ImageSize.UNKNOWN:
// full size
return '$imageId.jpg';
}
}
) =>
ProductImage.raw(
imgid: imageId.toString(),
size: imageSize,
).getUrlFilename();
}
Loading

0 comments on commit 3350251

Please sign in to comment.