I am scraping some images from any kind of websites. And I need to calculate their Size. For that I found that function which is working for most of the cases:
Future<Size> calculateImageDimension(String imageUrl) {
Completer<Size> completer = Completer();
late Image image;
image = Image.network(
imageUrl,
);
image.image.resolve(const ImageConfiguration()).addListener(
ImageStreamListener(
(ImageInfo image, bool synchronousCall) {
var myImage = image.image;
Size size = Size(myImage.width.toDouble(), myImage.height.toDouble());
if (!completer.isCompleted) {
completer.complete(size);
}
},
),
);
return completer.future;
}
Problem:
The function above fails with
Exception: Invalid image data
when calling it with this imageUrl:
https://static.zara.net/photos///contents/cm/media-transformations/joinlife-ctx/joinlife-large.svg?ts=1611919362013
What's the issue here? Also I couldn't catch the Exeption...
I found this related question. But its not helping me either.
I found this issue on Git and it works for my quite well. It is rather a workaround than a solution:
Future<bool> hasValidImageData(String url,BuildContext context) async {
bool hasNoError=true;
var output=Completer<bool>();
precacheImage(
NetworkImage(url),
context,
onError: (e,stackTrace)=>hasNoError=false,
).then((_)=>output.complete(hasNoError));
return output.future;
}
it makes use of the onError that precacheImage come with. A downside is that it needs the BuildContext but like I said, its working for me and the only workaround I found.
Related
I'm building a web app with flutter and I want an option to change the profile picture. To pick image from gallery I use the image_picker_web 2.1.1 package.
class ImagePickerService {
late Image? img = Image.asset('whatever'); //has to be initialised
Future<void> pickImage() async {
img = await ImagePickerWeb.getImageAsWidget();
}
}
I didn't set the type of pickImage() to Future<Image?> because then I'd have to convert from Future. This asset('whatever'), although not very elegant, doesn't cause any problems because before _fileLoaded is set to true while picking an image, I display username's initial letter as avatar. Without it I was getting an error of no initialisation.
Relevant snippets from settings page class:
late Image? avatar;
bool _fileLoaded = false;
final ImagePickerService _ips = ImagePickerService();
Center(
child: displayAvatar(),
),
TextButton.icon(
onPressed: () {
setState(() {
_ips.pickImage();
avatar = _ips.img;
_fileLoaded = true;
});
},
icon: const Icon(Icons.edit),
label: const Text('Change avatar'),
),
Widget displayAvatar() {
if (_fileLoaded) {
return CircleAvatar(
backgroundImage: avatar as ImageProvider, radius: 50.0);
} else...
I searched for similar problems, but didn't find any answer except for adding as ImageProvider, which in my case removes the error from IDE and lets me run the project, but the error appears later on the red screen when I press the button to change avatar, even though the type should be correct. Please, do you know any solution?
ImageProvider is an abstract class. Classes that implement ImageProvider are AssetImage(""), NetworkImage("URL"), FileImage() etc. I believe FileImage should be well suited for your case.
I'm posting the solution, in case someone has the same problem.
I switched back to the image_picker package, it supports web too, you just have to use it a bit differently:
class ImagePickerService {
final ImagePicker _picker = ImagePicker();
Uint8List picked = Uint8List(8);
Future<void> pickImage() async {
XFile? image = await _picker.pickImage(source: ImageSource.gallery);
if (image != null) picked = await image.readAsBytes();
}
}
ClipOval(
child: Image.memory(
avatar,
fit: BoxFit.cover,
width: 100.0,
height: 100.0,
),
)
avatar is assigned to _ips.picked, so it's of Uint8List type.
I have a method in my cubit that captures a widget as an image and I am testing this method. Long story short this method calls _captureFromWidget (see below, the code is copied from package screenshot), which accepts the Widget and returns a Uint8List. (I am not testing that the package works correctly, I am testing that my method and its parameters work correctly).
The problem is, that in real app the widget is captured correctly, but in the test, the font of all the Text widgets is not rendered correctly, and boxes are shown instead of letters.
I know the reason of this, see here, and here.
I tried loading the font as they suggested:
in pubspec.yaml:
assets:
- assets/fonts/
and I have in the assets/fonts folder my font: AbrilFatface-Regular.ttf
in my test:
final Future<ByteData> abrilFatFaceFontData = rootBundle.load('assets/fonts/AbrilFatface-Regular.ttf');
final FontLoader fontLoader = FontLoader('AbrilFatFace-Regular')..addFont(abrilFatFaceFontData);
await fontLoader.load();
Text text = ...; // text that uses custom font from above
cubit.captureWidget(...); // will invoke _captureFromWidget
but still, boxes are shown instead of letters in the captured image:
this is the result of calling the same methods with the same arguments from the real app:
So how to provide the font to the test correctly?
Here is the code that captures the widget:
/// [context] parameter is used to Inherit App Theme and MediaQuery data.
Future<Uint8List> _captureFromWidget(
widgets.Widget widget, {
required Duration delay,
double? pixelRatio,
widgets.BuildContext? context,
}) async {
// Retry counter
int retryCounter = 3;
bool isDirty = false;
widgets.Widget child = widget;
if (context != null) {
// Inherit Theme and MediaQuery of app
child = widgets.InheritedTheme.captureAll(
context,
widgets.MediaQuery(data: widgets.MediaQuery.of(context), child: child),
);
}
final RenderRepaintBoundary repaintBoundary = RenderRepaintBoundary();
Size logicalSize = ui.window.physicalSize / ui.window.devicePixelRatio;
Size imageSize = ui.window.physicalSize;
assert(logicalSize.aspectRatio.toPrecision(5) == imageSize.aspectRatio.toPrecision(5));
final RenderView renderView = RenderView(
window: ui.window,
child: RenderPositionedBox(alignment: Alignment.center, child: repaintBoundary),
configuration: ViewConfiguration(
size: logicalSize,
devicePixelRatio: pixelRatio ?? 1.0,
),
);
final PipelineOwner pipelineOwner = PipelineOwner();
final widgets.BuildOwner buildOwner = widgets.BuildOwner(
focusManager: widgets.FocusManager(),
onBuildScheduled: () {
///
///current render is dirty, mark it.
///
isDirty = true;
});
pipelineOwner.rootNode = renderView;
renderView.prepareInitialFrame();
final widgets.RenderObjectToWidgetElement<RenderBox> rootElement = widgets.RenderObjectToWidgetAdapter<RenderBox>(
container: repaintBoundary,
child: widgets.Directionality(
textDirection: TextDirection.ltr,
child: child,
)).attachToRenderTree(
buildOwner,
);
// Render Widget
buildOwner.buildScope(
rootElement,
);
buildOwner.finalizeTree();
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
ui.Image? image;
do {
// Reset the dirty flag
isDirty = false;
image = await repaintBoundary.toImage(pixelRatio: pixelRatio ?? (imageSize.width / logicalSize.width));
// This delay should increase with Widget tree Size
await Future.delayed(delay);
// Check does this require rebuild
if (isDirty) {
// Previous capture has been updated, re-render again.
buildOwner.buildScope(
rootElement,
);
buildOwner.finalizeTree();
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
}
retryCounter--;
//retry until capture is successful
} while (isDirty && retryCounter >= 0);
final ByteData? byteData = await image.toByteData(format: ui.ImageByteFormat.png);
return byteData!.buffer.asUint8List();
}
Turns out I was doing everything correctly except one thing.
The line final FontLoader fontLoader = FontLoader('AbrilFatFace-Regular')..addFont(abrilFatFaceFontData); will determine the fontFamily name that must be used in the TextStyle of the Text element.
and since I was using AbrilFatFace as fontFamily, the test was not working.
So the solution is to use in the fontFamily, the same name passed to the FontLoader, since thats the name the font will be identified with.
I'm trying to do a list of item from Firebase Firestore (this is done) and to get for each item a different image URL from Firebase Cloud Storage.
I use a function called getPhotoUrl to change the value of the variable photoUrl. The problem is that the return is executed before getPhotoUrl. If I add await in front of the function getPhotoUrl and async after _docs.map((document), I got an error saying that The argument type 'List<Future>' can't be assigned to the parameter type 'List'.
My code:
class PhotosList extends StatefulWidget {
#override
_PhotosListState createState() => _PhotosListState();
}
class _PhotosListState extends State<PhotosList> {
String photoUrl = 'lib/assets/default-image.png';
List<DocumentSnapshot> _docs;
getPhotoUrl(documentID) {
Reference ref = storage
.ref('Users')
.child(currentUser.uid)
.child('Photos')
.child(documentID)
.child('image_1.jpg');
ref.getDownloadURL().then((value) {
setState(() {
photoUrl = value.toString();
});
}).catchError((e) {
setState(() {
print(e.error);
});
});
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: firestore
.collection('Users')
.doc(currentUser.uid)
.collection('Photos')
.orderBy('date')
.snapshots(),
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
_docs = snapshot.data.docs;
if (_docs.isEmpty)
return Center(
child: Text("The list is empty."));
return Container(
child: ResponsiveGridList(
desiredItemWidth: 100,
squareCells: true,
minSpacing: 5,
children: _docs.map((document) {
getPhotoUrl(document.id);
return PhotosListItem(photoUrl: photoUrl);
}).toList(),
),
);
},
);
}
}
I think you mix 2 different ways. In every build cicle you map your docs and request that photoUrl, but inside that method you call setState, which re-triggers your build method. That way you should end in infinite loop of getting photo url and building your widget.
You have three options:
Load your photoUrls and store them inside your widget -> call set state -> check inside your mapping function if your photo is loaded, if yes, take it, if no, call your getPhotoUrl function
Load your photoUrls synchronously and return url from your function and set it to your PhotosListItem
(I would prefer this) Add your documentId to your photosListItem in your mapping function and inside your item you load this photo url. In this PhotoListItem you have a variable with your imageUrl and in initState you call your getPhotoUrl function
Inside your PhotoItem:
String imageUrl;
#override
void initState() {
Future.delayed(Duration.zero, () {
setState(() {
// load your data and set it to your variable
imageUrl = ..
});
});
super.initState();
}
You might use a FutureBuilder because StreamBuilder seems to be synchronous :
How to convert Future<List> to List in flutter?
Thanks for your answers guys, actually I found an other solution which is to get and write the URL in Firestore directly after uploading the image on the Storage.
This is the article which helped me a lot : https://medium.com/swlh/uploading-images-to-cloud-storage-using-flutter-130ac41741b2
(PS: some of the Firebase names changed since this article but it's still helpful.)
Regards.
My app load image from samba (instead of url) and then save thumbnail in local, so I can not use Image.network();, previouly I loaded image Uint8List from samba and then use Image.memory(bytes); to display image, it work, but I also have to combine it with Local Cache, so I save image file with flutter_cache_manager package DefaultCacheManager()(using (await DefaultCacheManager().getFileFromCache()).file), but the problem is, the DefaultCacheManager() only can return Future<File> cache instead of File, so I cannot use Image.file(cacheFile) to display image.
I have to try the FutureBuilder to solve the problem, but it cause flash in the image displaying.
I have considered to use FadeInImage and make my own ImageProvider to feed my need, but it is difficult for me to write ImageProvider.
In conclusion, I want to make something like:
Image.futureFile(Future<File>)
to use the Future cache return by DefaultCacheManager() to display a local image.If it cannot be solve(for example, a local cache file api should not return Future), I will conside to use another cache library.
Finally I end up with a custom ImageProvider, it refers to FileImage, I don't master at this so it may have problem (for example I don't test it with gif). After all, now image flashing is better than the FutureBuilder solution.
class CacheImageProvider extends ImageProvider<CacheImageProvider> {
final String fileId;//the cache id use to get cache
CacheImageProvider(this.fileId);
#override
ImageStreamCompleter load(CacheImageProvider key, DecoderCallback decode) {
return MultiFrameImageStreamCompleter(
codec: _loadAsync(decode),
scale: 1.0,
debugLabel: fileId,
informationCollector: () sync* {
yield ErrorDescription('Path: $fileId');
},
);
}
Future<Codec> _loadAsync(DecoderCallback decode) async {
// the DefaultCacheManager() encapsulation, it get cache from local storage.
final Uint8List bytes = await (await CacheThumbnail.getThumbnail(fileId)).readAsBytes();
if (bytes.lengthInBytes == 0) {
// The file may become available later.
PaintingBinding.instance?.imageCache?.evict(this);
throw StateError('$fileId is empty and cannot be loaded as an image.');
}
return await decode(bytes);
}
#override
Future<CacheImageProvider> obtainKey(ImageConfiguration configuration) {
return SynchronousFuture<CacheImageProvider>(this);
}
//the custom == and hashCode is need, because the ImageProvider use to get the memory cache.
#override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) return false;
bool res = other is CacheImageProvider && other.fileId == fileId;
return res;
}
#override
int get hashCode => fileId.hashCode;
#override
String toString() => '${objectRuntimeType(this, 'CacheImageProvider')}("$fileId")';
}
and use it:
Image(image: CacheImageProvider(_data[index].fileId))
// This should be in a callback or outside the build() function:
File _file = await DefaultCacheManager().getSingleFile(file);
// In the widget tree:
Image.file(_file);
Minimal Code:
File _file;
Future<void> _pickImage() async {
final image = await ImagePicker.pickImage(source: ImageSource.camera);
if (image != null) {
final file = File("${(await getApplicationDocumentsDirectory()).path}/image.png");
await file.writeAsBytes(await image.readAsBytes());
setState(() => _file = file); // `_file = image` works though
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
floatingActionButton: FloatingActionButton(child: Icon(Icons.camera_alt), onPressed: _pickImage),
body: _file == null ? Container() : Image.file(_file),
);
}
Watch video
As you can see, once I pick the image, it works, but on picking it second time, it doesn't work and I also don't run into any error. Can anyone please help?
you need 3 things:
first you have to use ImageProvider and its evict() method:
var image = FileImage(File('someImage.jpg'));
then you need Image widget that uses above ImageProvider and also assigns a unique key in order to be "different" each time build() method is called:
child: Image(
image: image,
key: UniqueKey(),
),
and finally after you overwrite someImage.jpg you have to call evict() method:
// part of your _pickImage() method
// here someImage.jpg contains updated content
image.evict();
setState(() {});
UPDATE: actually you dont need var image = FileImage(File('someImage.jpg')); - you can use it directly inside Image widget as image: FileImage(File('someImage.jpg')) and call FileImage(File('someImage.jpg')).evict() after your image is ovewritten