Chapter 13
Image Variations & Output Control
Multiple images, file type, orientation
Chapter 12 got you one image. This chapter covers two things you’ll actually want in a real feature: more than one option to choose from, and control over the shape that image comes back in.
Several images from one call
Same idea as generate_texts() from Chapter 10, applied to images:
$images = wp_ai_client_prompt( 'Aerial shot of snowy plains, cinematic.' )
->generate_images( 4 );
if ( is_wp_error( $images ) ) {
return;
}
foreach ( $images as $image_file ) {
echo '<img src="' . esc_url( $image_file->getDataUri(), array( 'data' ) ) . '">';
}
Notice esc_url() still takes that second argument, array( 'data' ), here and everywhere else in this chapter. That’s not optional decoration carried over from Chapter 12, leave it off and every image on this page breaks silently, same as it would have there.
generate_images() gives you back an array of File objects, not one. Same getDataUri() call on each, just looped.
Add this to chapter-13-image-variations.php inside includes:
<?php
/**
* Chapter 13: Image Variations and Output Control
* Usage: add [ai_course_ch13] anywhere, and [ai_course_ch13_landscape]
* inside an actual post or page (it needs a real title to work from).
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // No direct access.
}
use WordPress\AiClient\Files\Enums\FileTypeEnum;
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
function ai_course_ch13_image_grid() {
$images = wp_ai_client_prompt( 'A cozy WordPress-themed workspace, aerial view.' )
->generate_images( 4 );
if ( is_wp_error( $images ) ) {
return 'Could not generate images right now: ' . esc_html( $images->get_error_message() );
}
$output = '<div style="display:flex; flex-wrap:wrap; gap:10px;">';
foreach ( $images as $image_file ) {
$output .= '<img src="' . esc_url( $image_file->getDataUri(), array( 'data' ) ) . '" style="width:150px; height:auto;">';
}
$output .= '</div>';
return $output;
}
add_shortcode( 'ai_course_ch13', 'ai_course_ch13_image_grid' );
Add [ai_course_ch13] to a page and load it. Four different takes on the same prompt, in a row, the same “let a person pick” pattern from Chapter 10, just visual this time.
Warning: this specific demo has known issues on both major providers right now, worth knowing upfront rather than assuming your own code is broken if it doesn’t work first try.
On OpenAI, this chapter makes the same kind of image-generation call Chapter 12 does, twice over, so the same cURL timeout risk from Chapter 12’s Warning applies here too.
On Gemini, you may instead see “Multiple candidates is not enabled for this model,” Gemini telling you plainly that the connected image model doesn’t support generating more than one image per call. This isn’t a matter of picking a different Gemini model, Google’s own developer forum confirms it as a current platform-wide limitation across Gemini’s image generation models, including the newest ones. No code-level fix exists for that, the model itself doesn’t support what’s being asked.
using_model_preference() (an early preview, properly covered in Chapter 19) is worth trying if you’re on OpenAI, a different model might avoid the timeout. It won’t help on Gemini specifically for the multiple-candidates error, that one’s confirmed to hold across Gemini’s current image models, not something switching to a different one on the same provider fixes.
Getting a shape that actually fits your theme
Left alone, the model decides what shape to hand back, and there’s no guarantee it matches what your theme actually needs. A featured image slot at the top of a post is almost always wide. A related-posts thumbnail is often square. Getting back a tall portrait image for either one means cropping it badly or not using it at all.
as_output_media_orientation() fixes that:
use WordPress\AiClient\Files\Enums\MediaOrientationEnum;
->as_output_media_orientation( MediaOrientationEnum::from( 'landscape' ) )
Worth pausing on those use lines, since this is the first chapter in the book that needs one.
Every configuration method up to this point took a plain PHP value: a number for using_temperature(), a string for using_system_instruction(), an array for as_json_response(). Orientation and file type are different. They take an actual enum object from the AI Client’s underlying PHP library, WordPress\AiClient\Files\Enums\MediaOrientationEnum and WordPress\AiClient\Files\Enums\FileTypeEnum, not something WordPress wraps into a simple scalar.
PHP needs to know where that class lives before you can reference its short name inside your function, which is exactly what a use statement at the top of the file does. Without it, you’d have to write out \WordPress\AiClient\Files\Enums\MediaOrientationEnum::from( 'landscape' ) in full every time, which works but is a lot to type and read.
This is a small preview of something Chapter 28 covers properly: the AI Client is actually two layers, a WordPress wrapper that mostly speaks in plain snake_case functions and WP_Error, sitting on top of a provider-agnostic PHP SDK that speaks in proper classes and namespaces. Most of this book stays entirely in the WordPress-wrapper layer. Enums like these are one of the few places the underlying SDK’s classes show up directly in your code.
Add a second function to the same file:
function ai_course_ch13_landscape_featured_image() {
$post_title = get_the_title();
if ( empty( $post_title ) ) {
return 'Add this shortcode inside an actual post or page so there is a title to work from.';
}
$prompt = sprintf(
'A featured image concept for a blog post titled "%s". Editorial photography style, no text overlays.',
$post_title
);
$image_file = wp_ai_client_prompt( $prompt )
->as_output_file_type( FileTypeEnum::inline() )
->as_output_media_orientation( MediaOrientationEnum::from( 'landscape' ) )
->generate_image();
if ( is_wp_error( $image_file ) ) {
return 'Could not generate an image right now: ' . esc_html( $image_file->get_error_message() );
}
return '<img src="' . esc_url( $image_file->getDataUri(), array( 'data' ) ) . '" alt="Landscape-oriented featured image concept" style="max-width:100%; height:auto;">';
}
add_shortcode( 'ai_course_ch13_landscape', 'ai_course_ch13_landscape_featured_image' );
This is Chapter 12’s featured-image function again, same prompt built from get_the_title(), with two lines added on top. Add [ai_course_ch13_landscape] to the same post you tested Chapter 12 on and compare, this version should consistently come back wide, not whatever shape the model felt like giving you before.
as_output_file_type( FileTypeEnum::inline() ) is set explicitly here too. inline is the same behavior Chapter 12 used by default, a data URI, nothing saved anywhere. It’s worth setting it on purpose rather than relying on a default once you’re deliberately configuring output like this, and it also means when a future chapter needs the image saved as a real file instead, this is the exact line that changes.
A quick honesty note on this one
The official documentation shows as_output_file_type() and as_output_media_orientation() chained onto generate_image_result() specifically, not the plain generate_image() used above. I’ve used generate_image() here to stay consistent with Chapter 12 and avoid introducing the fuller result object early (that’s Chapter 18), since these are configuration methods on the builder and should apply regardless of which generation method reads the config back out. Worth confirming on your own install that generate_image() respects these settings the same way, and if it doesn’t, swapping to generate_image_result() is a one-line change, we’ll cover exactly what that method gives you extra in Module 5.
Try it yourself
Generate a square version and a landscape version of a featured image for the same post, using MediaOrientationEnum::from( 'square' ) for the first (the article only shows 'landscape' directly, 'square' and 'portrait' are reasonable guesses based on how the enum’s named, worth confirming against the MediaOrientationEnum class itself if 'square' throws an error on your setup). Compare which one actually looks right for your theme’s featured image spot, most themes crop hard on whatever aspect ratio you hand them, so this isn’t just a cosmetic setting, it’s the difference between an image that looks intentional and one that looks stretched or awkwardly cropped.