1+ import { existsSync , mkdirSync , writeFileSync } from 'node:fs' ;
2+ import { join , resolve , dirname } from 'node:path' ;
13import { Client } from "../client" ;
24import { imageEndpoint } from "../../client/endpoints" ;
35import { ImageRequest , ImageResponse } from "../../types/api" ;
46import { ModelPartial } from "../types" ;
57import { SDKError } from "../../errors/base" ;
68import { ExitCode } from "../../errors/codes" ;
9+ import { downloadFile } from "../../files/download" ;
710import { toMerged } from 'es-toolkit/object' ;
811
12+ export interface ImageSaveOptions {
13+ /** Save to exact file path (single image only). */
14+ out ?: string ;
15+ /** Save images to directory (default: "."). */
16+ outDir ?: string ;
17+ /** Filename prefix (default: "image"). */
18+ prefix ?: string ;
19+ /** Response format used by generate() — "url" or "base64" (default: "url"). */
20+ responseFormat ?: 'url' | 'base64' ;
21+ /** Suppress progress output. */
22+ quiet ?: boolean ;
23+ }
24+
925export class ImageSDK extends Client {
1026 async generate ( request : ModelPartial < ImageRequest > ) : Promise < ImageResponse > {
1127 const body = this . validateParams ( request ) ;
@@ -18,6 +34,69 @@ export class ImageSDK extends Client {
1834 } ) ;
1935 }
2036
37+ /**
38+ * Download and save images from a `generate()` response to disk.
39+ *
40+ * Handles both `"url"` (CDN download) and `"base64"` response formats
41+ * and creates intermediate directories as needed. Returns the absolute
42+ * paths of all saved files.
43+ */
44+ async save ( response : ImageResponse , options : ImageSaveOptions = { } ) : Promise < string [ ] > {
45+ const fmt = options . responseFormat || 'url' ;
46+ const quiet = options . quiet ?? true ;
47+ const saved : string [ ] = [ ] ;
48+
49+ if ( options . out ) {
50+ // Single-image, exact path
51+ const count = ( fmt === 'base64'
52+ ? ( response . data . image_base64 || [ ] ) . length
53+ : ( response . data . image_urls || [ ] ) . length ) ;
54+
55+ if ( count > 1 ) {
56+ throw new SDKError (
57+ 'Cannot use `out` with multiple images. Use `outDir` instead.' ,
58+ ExitCode . USAGE ,
59+ ) ;
60+ }
61+
62+ const destPath = resolve ( options . out ) ;
63+ const dir = dirname ( destPath ) ;
64+ if ( ! existsSync ( dir ) ) mkdirSync ( dir , { recursive : true } ) ;
65+
66+ if ( fmt === 'base64' ) {
67+ const image = ( response . data . image_base64 || [ ] ) [ 0 ] ;
68+ if ( image ) writeFileSync ( destPath , image , 'base64' ) ;
69+ } else {
70+ const imageUrl = ( response . data . image_urls || [ ] ) [ 0 ] ;
71+ if ( imageUrl ) await downloadFile ( imageUrl , destPath , { quiet } ) ;
72+ }
73+ saved . push ( destPath ) ;
74+ } else {
75+ // Multi-image, numbered filenames in a directory
76+ const outDir = resolve ( options . outDir || '.' ) ;
77+ if ( ! existsSync ( outDir ) ) mkdirSync ( outDir , { recursive : true } ) ;
78+ const prefix = options . prefix || 'image' ;
79+
80+ if ( fmt === 'base64' ) {
81+ const images = response . data . image_base64 || [ ] ;
82+ for ( let i = 0 ; i < images . length ; i ++ ) {
83+ const destPath = join ( outDir , `${ prefix } _${ String ( i + 1 ) . padStart ( 3 , '0' ) } .jpg` ) ;
84+ writeFileSync ( destPath , images [ i ] ! , 'base64' ) ;
85+ saved . push ( destPath ) ;
86+ }
87+ } else {
88+ const imageUrls = response . data . image_urls || [ ] ;
89+ for ( let i = 0 ; i < imageUrls . length ; i ++ ) {
90+ const destPath = join ( outDir , `${ prefix } _${ String ( i + 1 ) . padStart ( 3 , '0' ) } .jpg` ) ;
91+ await downloadFile ( imageUrls [ i ] ! , destPath , { quiet } ) ;
92+ saved . push ( destPath ) ;
93+ }
94+ }
95+ }
96+
97+ return saved ;
98+ }
99+
21100 private validateParams ( params : Partial < ImageRequest > ) : ImageRequest {
22101 const { width, height, aspect_ratio } = params ;
23102
0 commit comments