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: simplified access to raw product images #839

Merged
merged 8 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from 7 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
27 changes: 25 additions & 2 deletions lib/src/model/product.dart
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,33 @@ 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, like "picture 12" for "400" size.
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
List<ProductImage>? getRawImages() => _getImageSubset(false);

/// "Main" images, like "front" picture for "French" and "400" size.
monsieurtanuki marked this conversation as resolved.
Show resolved Hide resolved
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