Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
final FocusNode _textFieldFocus = FocusNode();
final List<MessageData> _messages = <MessageData>[];
bool _loading = false;
ImageAspectRatio? _selectedAspectRatio;
ImageSize? _selectedImageSize;

@override
void initState() {
Expand Down Expand Up @@ -80,7 +82,19 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
_scrollDown();

try {
final response = await _model.generateContent([Content.text(prompt)]);
final response = await _model.generateContent(
[Content.text(prompt)],
generationConfig: GenerationConfig(
responseModalities: [
ResponseModalities.text,
ResponseModalities.image,
],
imageConfig: ImageConfig(
aspectRatio: _selectedAspectRatio,
imageSize: _selectedImageSize,
),
),
);

String? textResponse = response.text;
Uint8List? imageBytes;
Expand Down Expand Up @@ -162,31 +176,100 @@ class _ImageGenerationPageState extends State<ImageGenerationPage> {
),
),
Padding(
padding: const EdgeInsets.symmetric(vertical: 25, horizontal: 15),
child: Row(
padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 15),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: TextField(
autofocus: true,
focusNode: _textFieldFocus,
controller: _textController,
decoration: const InputDecoration(
hintText: 'Enter image prompt...',
Row(
children: [
Expanded(
child: DropdownButtonFormField<ImageAspectRatio?>(
initialValue: _selectedAspectRatio,
decoration: const InputDecoration(
labelText: 'Aspect Ratio',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
),
items: [
const DropdownMenuItem(
child: Text('Default'),
),
...ImageAspectRatio.values.map(
(e) => DropdownMenuItem(
value: e,
child: Text('${e.name} (${e.toJson()})'),
),
),
],
onChanged: (value) {
setState(() {
_selectedAspectRatio = value;
});
},
),
),
onSubmitted: _generateImage,
),
const SizedBox(width: 15),
Expanded(
child: DropdownButtonFormField<ImageSize?>(
initialValue: _selectedImageSize,
decoration: const InputDecoration(
labelText: 'Image Size',
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(
horizontal: 10,
vertical: 10,
),
),
items: [
const DropdownMenuItem(
child: Text('Default'),
),
...ImageSize.values.map(
(e) => DropdownMenuItem(
value: e,
child: Text('${e.name} (${e.toJson()})'),
),
),
],
onChanged: (value) {
setState(() {
_selectedImageSize = value;
});
},
),
),
],
),
const SizedBox(width: 15),
if (!_loading)
IconButton(
onPressed: () => _generateImage(_textController.text),
icon: Icon(
Icons.send,
color: Theme.of(context).colorScheme.primary,
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: TextField(
autofocus: true,
focusNode: _textFieldFocus,
controller: _textController,
decoration: const InputDecoration(
hintText: 'Enter image prompt...',
),
onSubmitted: _generateImage,
),
),
)
else
const CircularProgressIndicator(),
const SizedBox(width: 15),
if (!_loading)
IconButton(
onPressed: () => _generateImage(_textController.text),
icon: Icon(
Icons.send,
color: Theme.of(context).colorScheme.primary,
),
)
else
const CircularProgressIndicator(),
],
),
],
),
),
Expand Down
1 change: 1 addition & 0 deletions packages/firebase_ai/firebase_ai/lib/firebase_ai.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export 'src/error.dart'
QuotaExceeded,
UnsupportedUserLocation;
export 'src/firebase_ai.dart' show FirebaseAI;
export 'src/image_config.dart' show ImageConfig, ImageAspectRatio, ImageSize;
export 'src/imagen/imagen_api.dart'
show
ImagenSafetySettings,
Expand Down
60 changes: 58 additions & 2 deletions packages/firebase_ai/firebase_ai/lib/src/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import 'content.dart';
import 'error.dart';
import 'image_config.dart';
import 'schema.dart';
import 'tool.dart' show Tool, ToolConfig;

Expand Down Expand Up @@ -790,9 +791,45 @@ enum FinishReason {
/// The candidate content was flagged for malformed function call reasons.
malformedFunctionCall('MALFORMED_FUNCTION_CALL'),

/// The model produced an unexpected tool call.
/// Token generation was stopped because the response contained forbidden terms.
blocklist('BLOCKLIST'),

/// Token generation was stopped because the response contained potentially prohibited content.
prohibitedContent('PROHIBITED_CONTENT'),

/// Token generation was stopped because of Sensitive Personally Identifiable Information (SPII).
spii('SPII'),

/// Token generation stopped because generated images contain safety violations.
imageSafety('IMAGE_SAFETY'),

/// Image generation stopped because generated images have other prohibited content.
imageProhibitedContent('IMAGE_PROHIBITED_CONTENT'),

/// Image generation stopped because of other miscellaneous issues.
imageOther('IMAGE_OTHER'),

/// The model was expected to generate an image, but none was generated.
noImage('NO_IMAGE'),

/// Image generation stopped due to recitation.
imageRecitation('IMAGE_RECITATION'),

/// The response candidate content was flagged for using an unsupported language.
language('LANGUAGE'),

/// Model generated a tool call but no tools were enabled in the request.
unexpectedToolCall('UNEXPECTED_TOOL_CALL'),

/// Model called too many tools consecutively, thus the system exited execution.
tooManyToolCalls('TOO_MANY_TOOL_CALLS'),

/// Request has at least one thought signature missing.
missingThoughtSignature('MISSING_THOUGHT_SIGNATURE'),

/// Finished due to malformed response.
malformedResponse('MALFORMED_RESPONSE'),

/// Unknown reason.
other('OTHER');

Expand All @@ -813,8 +850,21 @@ enum FinishReason {
'RECITATION' => FinishReason.recitation,
'OTHER' => FinishReason.other,
'MALFORMED_FUNCTION_CALL' => FinishReason.malformedFunctionCall,
'BLOCKLIST' => FinishReason.blocklist,
'PROHIBITED_CONTENT' => FinishReason.prohibitedContent,
'SPII' => FinishReason.spii,
'IMAGE_SAFETY' => FinishReason.imageSafety,
'IMAGE_PROHIBITED_CONTENT' => FinishReason.imageProhibitedContent,
'IMAGE_OTHER' => FinishReason.imageOther,
'NO_IMAGE' => FinishReason.noImage,
'IMAGE_RECITATION' => FinishReason.imageRecitation,
'LANGUAGE' => FinishReason.language,
'UNEXPECTED_TOOL_CALL' => FinishReason.unexpectedToolCall,
_ => throw FormatException('Unhandled FinishReason format', jsonObject),
'TOO_MANY_TOOL_CALLS' => FinishReason.tooManyToolCalls,
'MISSING_THOUGHT_SIGNATURE' => FinishReason.missingThoughtSignature,
'MALFORMED_RESPONSE' => FinishReason.malformedResponse,
'UNKNOWN' => FinishReason.unknown,
_ => FinishReason.unknown,
};
}

Expand Down Expand Up @@ -1220,6 +1270,7 @@ final class GenerationConfig extends BaseGenerationConfig {
this.responseSchema,
this.responseJsonSchema,
this.thinkingConfig,
this.imageConfig,
}) : assert(responseSchema == null || responseJsonSchema == null,
'responseSchema and responseJsonSchema cannot both be set.');

Expand Down Expand Up @@ -1268,6 +1319,9 @@ final class GenerationConfig extends BaseGenerationConfig {
/// support thinking.
final ThinkingConfig? thinkingConfig;

/// Configuration options for generating images with Gemini models.
final ImageConfig? imageConfig;

@override
Map<String, Object?> toJson() => {
...super.toJson(),
Expand All @@ -1282,6 +1336,8 @@ final class GenerationConfig extends BaseGenerationConfig {
'responseJsonSchema': responseJsonSchema,
if (thinkingConfig case final thinkingConfig?)
'thinkingConfig': thinkingConfig.toJson(),
if (imageConfig case final imageConfig?)
'imageConfig': imageConfig.toJson(),
};
}

Expand Down
110 changes: 110 additions & 0 deletions packages/firebase_ai/firebase_ai/lib/src/image_config.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2026 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/// Configuration options for generating images with Gemini models.
final class ImageConfig {
/// Initializes configuration options for generating images with Gemini.
const ImageConfig({this.aspectRatio, this.imageSize});

/// The aspect ratio of generated images.
final ImageAspectRatio? aspectRatio;

/// The size of the generated images.
final ImageSize? imageSize;

/// Convert to json format.
Map<String, Object?> toJson() => {
if (aspectRatio case final aspectRatio?)
'aspectRatio': aspectRatio.toJson(),
if (imageSize case final imageSize?) 'imageSize': imageSize.toJson(),
};
}

/// An aspect ratio for generated images.
enum ImageAspectRatio {
/// Square (1:1) aspect ratio.
square1x1('1:1'),

/// Portrait widescreen (9:16) aspect ratio.
portrait9x16('9:16'),

/// Widescreen (16:9) aspect ratio.
landscape16x9('16:9'),

/// Portrait full screen (3:4) aspect ratio.
portrait3x4('3:4'),

/// Fullscreen (4:3) aspect ratio.
landscape4x3('4:3'),

/// Portrait (2:3) aspect ratio.
portrait2x3('2:3'),

/// Landscape (3:2) aspect ratio.
landscape3x2('3:2'),

/// Portrait (4:5) aspect ratio.
portrait4x5('4:5'),

/// Landscape (5:4) aspect ratio.
landscape5x4('5:4'),

/// Portrait (1:4) aspect ratio.
portrait1x4('1:4'),

/// Landscape (4:1) aspect ratio.
landscape4x1('4:1'),

/// Portrait (1:8) aspect ratio.
portrait1x8('1:8'),

/// Landscape (8:1) aspect ratio.
landscape8x1('8:1'),

/// Ultrawide (21:9) aspect ratio.
ultrawide21x9('21:9');

const ImageAspectRatio(this._jsonString);
final String _jsonString;

/// Convert to json format.
String toJson() => _jsonString;

@override
String toString() => name;
}
Comment thread
paulb777 marked this conversation as resolved.

/// The size of images to generate.
enum ImageSize {
/// 512px (0.5K) image size.
size512('512'),

/// 1K image size.
size1K('1K'),

/// 2K image size.
size2K('2K'),

/// 4K image size.
size4K('4K');

const ImageSize(this._jsonString);
final String _jsonString;

/// Convert to json format.
String toJson() => _jsonString;

@override
String toString() => name;
}
Comment thread
paulb777 marked this conversation as resolved.
Loading
Loading