Skip to content

Commit d252f15

Browse files
huanduAlexV525
andauthored
Add new option parse_animation to parse metadata for animated images (#676)
## What does this change? Resolves #675 - [x] New feature (non-breaking change which adds functionality) - [x] This change requires a documentation update --------- Co-authored-by: Alex Li <[email protected]>
1 parent 63abef6 commit d252f15

37 files changed

+979
-63
lines changed

README.md

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ fluttergen -c example/pubspec.yaml
135135
## Configuration file
136136

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

140140
```yaml
141141
# pubspec.yaml
@@ -173,7 +173,6 @@ flutter:
173173

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

176-
177176
```yaml
178177
# build.yaml
179178
# ...
@@ -279,7 +278,9 @@ Widget build(BuildContext context) {
279278
);
280279
}
281280
```
281+
282282
or
283+
283284
```dart
284285
// Explicit usage for `Image`/`SvgPicture`/`Lottie`.
285286
Widget build(BuildContext context) {
@@ -343,6 +344,32 @@ Widget build(BuildContext context) {
343344
}
344345
```
345346

347+
You can use `parse_animation` to generate more animation details.
348+
It will automatically parse all animation information for GIF and WebP files,
349+
including frames, duration, etc. As this option significantly increases generation time,
350+
The option is disabled by default; enabling it will significantly increase the generation elapse.
351+
352+
```yaml
353+
flutter_gen:
354+
images:
355+
parse_animation: true # <- Add this line (default: false)
356+
# This option implies parse_metadata: true when parsing images.
357+
```
358+
359+
For GIF and WebP animation, several new nullable field is added to the
360+
generated class. For example:
361+
362+
```dart
363+
AssetGenImage get animated =>
364+
const AssetGenImage(
365+
'assets/images/animated.webp',
366+
size: Size(209.0, 49.0),
367+
isAnimation: true,
368+
duration: Duration(milliseconds: 1000),
369+
frames: 15,
370+
);
371+
```
372+
346373
#### Usage Example
347374

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

583610
#### Bad State: No Element when using build_runner
611+
584612
If you get an error message like this:
613+
585614
```
586615
[SEVERE] flutter_gen_runner:flutter_gen_runner on $package$:
587616
@@ -620,8 +649,6 @@ output-localization-file: app_localizations.dart
620649
synthetic-package: false <--- ⚠️Add this line⚠️
621650
```
622651

623-
If you get
624-
625652
## Contributing
626653

627654
**We are looking for co-developers.**

examples/example/lib/gen/assets.gen.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,12 +242,14 @@ class AssetGenImage {
242242
this._assetName, {
243243
this.size,
244244
this.flavors = const {},
245+
this.animation,
245246
});
246247

247248
final String _assetName;
248249

249250
final Size? size;
250251
final Set<String> flavors;
252+
final AssetGenImageAnimation? animation;
251253

252254
Image image({
253255
Key? key,
@@ -318,6 +320,18 @@ class AssetGenImage {
318320
String get keyName => _assetName;
319321
}
320322

323+
class AssetGenImageAnimation {
324+
const AssetGenImageAnimation({
325+
required this.isAnimation,
326+
required this.duration,
327+
required this.frames,
328+
});
329+
330+
final bool isAnimation;
331+
final Duration duration;
332+
final int frames;
333+
}
334+
321335
class SvgGenImage {
322336
const SvgGenImage(
323337
this._assetName, {

examples/example_resources/lib/gen/assets.gen.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ class AssetGenImage {
7070
this._assetName, {
7171
this.size,
7272
this.flavors = const {},
73+
this.animation,
7374
});
7475

7576
final String _assetName;
@@ -78,6 +79,7 @@ class AssetGenImage {
7879

7980
final Size? size;
8081
final Set<String> flavors;
82+
final AssetGenImageAnimation? animation;
8183

8284
Image image({
8385
Key? key,
@@ -150,6 +152,18 @@ class AssetGenImage {
150152
String get keyName => 'packages/example_resources/$_assetName';
151153
}
152154

155+
class AssetGenImageAnimation {
156+
const AssetGenImageAnimation({
157+
required this.isAnimation,
158+
required this.duration,
159+
required this.frames,
160+
});
161+
162+
final bool isAnimation;
163+
final Duration duration;
164+
final int frames;
165+
}
166+
153167
class SvgGenImage {
154168
const SvgGenImage(
155169
this._assetName, {

packages/command/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ executables:
1414

1515
dependencies:
1616
flutter_gen_core: 5.10.0
17+
1718
args: ^2.0.0
1819
logging: ^1.3.0
1920

packages/core/lib/generators/assets_generator.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ Future<String> generateAssets(
6363
ImageIntegration(
6464
config.packageParameterLiteral,
6565
parseMetadata: config.flutterGen.parseMetadata,
66+
parseAnimation: config.flutterGen.images.parseAnimation,
6667
),
6768
if (config.flutterGen.integrations.flutterSvg)
6869
SvgIntegration(

packages/core/lib/generators/integrations/image_integration.dart

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:io';
22

33
import 'package:flutter_gen_core/generators/integrations/integration.dart';
44
import 'package:flutter_gen_core/utils/log.dart';
5+
import 'package:image/image.dart' as img;
56
import 'package:image_size_getter/file_input.dart';
67
import 'package:image_size_getter/image_size_getter.dart';
78

@@ -12,9 +13,12 @@ import 'package:image_size_getter/image_size_getter.dart';
1213
class ImageIntegration extends Integration {
1314
ImageIntegration(
1415
String packageName, {
15-
super.parseMetadata,
16+
required super.parseMetadata,
17+
required this.parseAnimation,
1618
}) : super(packageName);
1719

20+
final bool parseAnimation;
21+
1822
String get packageParameter => isPackage ? ' = package' : '';
1923

2024
String get keyName =>
@@ -33,6 +37,7 @@ class ImageIntegration extends Integration {
3337
this._assetName, {
3438
this.size,
3539
this.flavors = const {},
40+
this.animation,
3641
});
3742
3843
final String _assetName;
@@ -41,6 +46,7 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
4146
4247
final Size? size;
4348
final Set<String> flavors;
49+
final AssetGenImageAnimation? animation;
4450
4551
Image image({
4652
Key? key,
@@ -110,19 +116,41 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
110116
111117
String get keyName => $keyName;
112118
}
119+
120+
class AssetGenImageAnimation {
121+
const AssetGenImageAnimation({
122+
required this.isAnimation,
123+
required this.duration,
124+
required this.frames,
125+
});
126+
127+
final bool isAnimation;
128+
final Duration duration;
129+
final int frames;
130+
}
113131
''';
114132

115133
@override
116134
String get className => 'AssetGenImage';
117135

118136
@override
119137
String classInstantiate(AssetType asset) {
120-
final info = parseMetadata ? _getMetadata(asset) : null;
138+
final info = parseMetadata || parseAnimation ? _getMetadata(asset) : null;
121139
final buffer = StringBuffer(className);
122140
buffer.write('(');
123141
buffer.write('\'${asset.posixStylePath}\'');
124142
if (info != null) {
125-
buffer.write(', size: Size(${info.width}, ${info.height})');
143+
buffer.write(', size: const Size(${info.width}, ${info.height})');
144+
145+
if (info.animation case final animation?) {
146+
buffer.write(', animation: const AssetGenImageAnimation(');
147+
buffer.write('isAnimation: ${animation.frames > 1}');
148+
buffer.write(
149+
', duration: Duration(milliseconds: ${animation.duration.inMilliseconds})',
150+
);
151+
buffer.write(', frames: ${animation.frames}');
152+
buffer.write(')');
153+
}
126154
}
127155
if (asset.flavors.isNotEmpty) {
128156
buffer.write(', flavors: {');
@@ -162,10 +190,53 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
162190
FileInput(File(asset.fullPath)),
163191
);
164192
final size = result.size;
165-
return ImageMetadata(size.width.toDouble(), size.height.toDouble());
193+
final animation = parseAnimation ? _parseAnimation(asset) : null;
194+
195+
return ImageMetadata(
196+
width: size.width.toDouble(),
197+
height: size.height.toDouble(),
198+
animation: animation,
199+
);
166200
} catch (e, s) {
167201
log.warning('Failed to parse \'${asset.path}\' metadata.', e, s);
168202
}
169203
return null;
170204
}
205+
206+
ImageAnimation? _parseAnimation(AssetType asset) {
207+
try {
208+
final decoder = switch (asset.mime) {
209+
'image/gif' => img.GifDecoder(),
210+
'image/webp' => img.WebPDecoder(),
211+
_ => null,
212+
};
213+
214+
if (decoder == null) {
215+
return null;
216+
}
217+
218+
final file = File(asset.fullPath);
219+
final bytes = file.readAsBytesSync();
220+
final image = decoder.decode(bytes);
221+
222+
if (image == null) {
223+
return null;
224+
}
225+
226+
return ImageAnimation(
227+
frames: image.frames.length,
228+
duration: Duration(
229+
milliseconds: image.frames.fold(
230+
0,
231+
(duration, frame) => duration + frame.frameDuration,
232+
),
233+
),
234+
);
235+
} catch (e) {
236+
stderr.writeln(
237+
'[WARNING] Failed to parse \'${asset.path}\' animation information: $e',
238+
);
239+
}
240+
return null;
241+
}
171242
}

packages/core/lib/generators/integrations/integration.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,24 @@ const String deprecationMessagePackage =
5555
/// Currently only contains the width and height, but could contain more in
5656
/// future.
5757
class ImageMetadata {
58-
const ImageMetadata(this.width, this.height);
58+
const ImageMetadata({
59+
required this.width,
60+
required this.height,
61+
this.animation,
62+
});
5963

6064
final double width;
6165
final double height;
66+
final ImageAnimation? animation;
67+
}
68+
69+
/// Metadata about the parsed animation file when [parseAnimation] is true.
70+
class ImageAnimation {
71+
const ImageAnimation({
72+
required this.frames,
73+
required this.duration,
74+
});
75+
76+
final int frames;
77+
final Duration duration;
6278
}

packages/core/lib/generators/integrations/svg_integration.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ import 'package:flutter_gen_core/utils/log.dart';
55
import 'package:vector_graphics_compiler/vector_graphics_compiler.dart';
66

77
class SvgIntegration extends Integration {
8-
SvgIntegration(String packageName, {super.parseMetadata})
9-
: super(packageName);
8+
SvgIntegration(
9+
String packageName, {
10+
super.parseMetadata,
11+
}) : super(packageName);
1012

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

@@ -138,7 +140,10 @@ ${isPackage ? "\n static const String package = '$packageName';" : ''}
138140
// but it's also the same way it will be eventually rendered by Flutter.
139141
final svg = File(asset.fullPath).readAsStringSync();
140142
final vec = parseWithoutOptimizers(svg);
141-
return ImageMetadata(vec.width, vec.height);
143+
return ImageMetadata(
144+
width: vec.width,
145+
height: vec.height,
146+
);
142147
} catch (e, s) {
143148
log.warning('Failed to parse SVG \'${asset.path}\' metadata.', e, s);
144149
return null;

packages/core/lib/settings/config_default.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ flutter_gen:
1212
flutter_svg: false
1313
rive: false
1414
lottie: false
15+
16+
images:
17+
# Optional
18+
parse_animation: false
1519
1620
assets:
1721
enabled: true # Optional

0 commit comments

Comments
 (0)