Skip to content

Add new option parse_animation to parse metadata for animated images #676

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

Merged
merged 19 commits into from
Jul 17, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ fluttergen -c example/pubspec.yaml
## Configuration file

[FlutterGen] generates dart files based on the key **`flutter`** and **`flutter_gen`** of [`pubspec.yaml`](https://dart.dev/tools/pub/pubspec).
Default configuration can be found [here](https://github.com/FlutterGen/flutter_gen/tree/main/packages/core/lib/settings/config_default.dart).
Default configuration can be found [here](https://github.com/FlutterGen/flutter_gen/tree/main/packages/core/lib/settings/config_default.dart).

```yaml
# pubspec.yaml
Expand Down Expand Up @@ -173,7 +173,6 @@ flutter:

You can also configure generate options in the `build.yaml`, it will be read before the `pubspec.yaml` if it exists.


```yaml
# build.yaml
# ...
Expand Down Expand Up @@ -279,7 +278,9 @@ Widget build(BuildContext context) {
);
}
```

or

```dart
// Explicit usage for `Image`/`SvgPicture`/`Lottie`.
Widget build(BuildContext context) {
Expand Down Expand Up @@ -343,6 +344,32 @@ Widget build(BuildContext context) {
}
```

You can use `parse_animation` to generate more animation details.
It will automatically parse all animation information for GIF and WebP files,
including frames, duration, etc. As this option significantly increases generation time,
The option is disabled by default; enabling it will significantly increase the generation elapse.

```yaml
flutter_gen:
images:
parse_animation: true # <- Add this line (default: false)
# This option implies parse_metadata: true when parsing images.
```

For GIF and WebP animation, several new nullable field is added to the
generated class. For example:

```dart
AssetGenImage get animated =>
const AssetGenImage(
'assets/images/animated.webp',
size: Size(209.0, 49.0),
isAnimation: true,
duration: Duration(milliseconds: 1000),
frames: 15,
);
```

#### Usage Example

[FlutterGen] generates [Image](https://api.flutter.dev/flutter/widgets/Image-class.html) class if the asset is Flutter supported image format.
Expand Down Expand Up @@ -581,7 +608,9 @@ Plugin issues that are not specific to [FlutterGen] can be filed in the [Flutter
### Known Issues

#### Bad State: No Element when using build_runner

If you get an error message like this:

```
[SEVERE] flutter_gen_runner:flutter_gen_runner on $package$:

Expand Down Expand Up @@ -620,8 +649,6 @@ output-localization-file: app_localizations.dart
synthetic-package: false <--- ⚠️Add this line⚠️
```

If you get

## Contributing

**We are looking for co-developers.**
Expand Down
14 changes: 14 additions & 0 deletions examples/example/lib/gen/assets.gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -242,12 +242,14 @@ class AssetGenImage {
this._assetName, {
this.size,
this.flavors = const {},
this.animation,
});

final String _assetName;

final Size? size;
final Set<String> flavors;
final AssetGenImageAnimation? animation;

Image image({
Key? key,
Expand Down Expand Up @@ -318,6 +320,18 @@ class AssetGenImage {
String get keyName => _assetName;
}

class AssetGenImageAnimation {
const AssetGenImageAnimation({
required this.isAnimation,
required this.duration,
required this.frames,
});

final bool isAnimation;
final Duration duration;
final int frames;
}

class SvgGenImage {
const SvgGenImage(
this._assetName, {
Expand Down
14 changes: 14 additions & 0 deletions examples/example_resources/lib/gen/assets.gen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class AssetGenImage {
this._assetName, {
this.size,
this.flavors = const {},
this.animation,
});

final String _assetName;
Expand All @@ -78,6 +79,7 @@ class AssetGenImage {

final Size? size;
final Set<String> flavors;
final AssetGenImageAnimation? animation;

Image image({
Key? key,
Expand Down Expand Up @@ -150,6 +152,18 @@ class AssetGenImage {
String get keyName => 'packages/example_resources/$_assetName';
}

class AssetGenImageAnimation {
const AssetGenImageAnimation({
required this.isAnimation,
required this.duration,
required this.frames,
});

final bool isAnimation;
final Duration duration;
final int frames;
}

class SvgGenImage {
const SvgGenImage(
this._assetName, {
Expand Down
1 change: 1 addition & 0 deletions packages/command/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ executables:

dependencies:
flutter_gen_core: 5.10.0

args: ^2.0.0
logging: ^1.3.0

Expand Down
1 change: 1 addition & 0 deletions packages/core/lib/generators/assets_generator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ Future<String> generateAssets(
ImageIntegration(
config.packageParameterLiteral,
parseMetadata: config.flutterGen.parseMetadata,
parseAnimation: config.flutterGen.images.parseAnimation,
),
if (config.flutterGen.integrations.flutterSvg)
SvgIntegration(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:io';

import 'package:flutter_gen_core/generators/integrations/integration.dart';
import 'package:flutter_gen_core/utils/log.dart';
import 'package:image/image.dart' as img;
import 'package:image_size_getter/file_input.dart';
import 'package:image_size_getter/image_size_getter.dart';

Expand All @@ -12,9 +13,12 @@ import 'package:image_size_getter/image_size_getter.dart';
class ImageIntegration extends Integration {
ImageIntegration(
String packageName, {
super.parseMetadata,
required super.parseMetadata,
required this.parseAnimation,
}) : super(packageName);

final bool parseAnimation;

String get packageParameter => isPackage ? ' = package' : '';

String get keyName =>
Expand All @@ -33,6 +37,7 @@ class ImageIntegration extends Integration {
this._assetName, {
this.size,
this.flavors = const {},
this.animation,
});

final String _assetName;
Expand All @@ -41,6 +46,7 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}

final Size? size;
final Set<String> flavors;
final AssetGenImageAnimation? animation;

Image image({
Key? key,
Expand Down Expand Up @@ -110,19 +116,41 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}

String get keyName => $keyName;
}

class AssetGenImageAnimation {
const AssetGenImageAnimation({
required this.isAnimation,
required this.duration,
required this.frames,
});

final bool isAnimation;
final Duration duration;
final int frames;
}
''';

@override
String get className => 'AssetGenImage';

@override
String classInstantiate(AssetType asset) {
final info = parseMetadata ? _getMetadata(asset) : null;
final info = parseMetadata || parseAnimation ? _getMetadata(asset) : null;
final buffer = StringBuffer(className);
buffer.write('(');
buffer.write('\'${asset.posixStylePath}\'');
if (info != null) {
buffer.write(', size: Size(${info.width}, ${info.height})');
buffer.write(', size: const Size(${info.width}, ${info.height})');

if (info.animation case final animation?) {
buffer.write(', animation: const AssetGenImageAnimation(');
buffer.write('isAnimation: ${animation.frames > 1}');
buffer.write(
', duration: Duration(milliseconds: ${animation.duration.inMilliseconds})',
);
buffer.write(', frames: ${animation.frames}');
buffer.write(')');
}
}
if (asset.flavors.isNotEmpty) {
buffer.write(', flavors: {');
Expand Down Expand Up @@ -162,10 +190,53 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
FileInput(File(asset.fullPath)),
);
final size = result.size;
return ImageMetadata(size.width.toDouble(), size.height.toDouble());
final animation = parseAnimation ? _parseAnimation(asset) : null;

return ImageMetadata(
width: size.width.toDouble(),
height: size.height.toDouble(),
animation: animation,
);
} catch (e, s) {
log.warning('Failed to parse \'${asset.path}\' metadata.', e, s);
}
return null;
}

ImageAnimation? _parseAnimation(AssetType asset) {
try {
final decoder = switch (asset.mime) {
'image/gif' => img.GifDecoder(),
'image/webp' => img.WebPDecoder(),
_ => null,
};

if (decoder == null) {
return null;
}

final file = File(asset.fullPath);
final bytes = file.readAsBytesSync();
final image = decoder.decode(bytes);

if (image == null) {
return null;
}

return ImageAnimation(
frames: image.frames.length,
duration: Duration(
milliseconds: image.frames.fold(
0,
(duration, frame) => duration + frame.frameDuration,
),
),
);
} catch (e) {
stderr.writeln(
'[WARNING] Failed to parse \'${asset.path}\' animation information: $e',
);
}
return null;
}
}
18 changes: 17 additions & 1 deletion packages/core/lib/generators/integrations/integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,24 @@ const String deprecationMessagePackage =
/// Currently only contains the width and height, but could contain more in
/// future.
class ImageMetadata {
const ImageMetadata(this.width, this.height);
const ImageMetadata({
required this.width,
required this.height,
this.animation,
});

final double width;
final double height;
final ImageAnimation? animation;
}

/// Metadata about the parsed animation file when [parseAnimation] is true.
class ImageAnimation {
const ImageAnimation({
required this.frames,
required this.duration,
});

final int frames;
final Duration duration;
}
11 changes: 8 additions & 3 deletions packages/core/lib/generators/integrations/svg_integration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import 'package:flutter_gen_core/utils/log.dart';
import 'package:vector_graphics_compiler/vector_graphics_compiler.dart';

class SvgIntegration extends Integration {
SvgIntegration(String packageName, {super.parseMetadata})
: super(packageName);
SvgIntegration(
String packageName, {
super.parseMetadata,
}) : super(packageName);

String get packageExpression => isPackage ? ' = package' : '';

Expand Down Expand Up @@ -138,7 +140,10 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
// but it's also the same way it will be eventually rendered by Flutter.
final svg = File(asset.fullPath).readAsStringSync();
final vec = parseWithoutOptimizers(svg);
return ImageMetadata(vec.width, vec.height);
return ImageMetadata(
width: vec.width,
height: vec.height,
);
} catch (e, s) {
log.warning('Failed to parse SVG \'${asset.path}\' metadata.', e, s);
return null;
Expand Down
4 changes: 4 additions & 0 deletions packages/core/lib/settings/config_default.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ flutter_gen:
flutter_svg: false
rive: false
lottie: false

images:
# Optional
parse_animation: false

assets:
enabled: true # Optional
Expand Down
Loading
Loading