Skip to content
Open
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
32 changes: 32 additions & 0 deletions lib/api_client/call_generate_api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const utils = require("../utils");
const config = require("../config");
const ensureOption = require('../utils/ensureOption').defaults(config());
const execute_request = require("./execute_request");

const {ensurePresenceOf} = utils;

function call_generate_api(method, uri, params, callback, options) {
ensurePresenceOf({
method,
uri
});
const api_url = utils.base_processing_api_url()(uri, options);
let auth = {};
if (options.oauth_token || config().oauth_token) {
auth = {
oauth_token: ensureOption(options, "oauth_token")
};
} else {
auth = {
key: ensureOption(options, "api_key"),
secret: ensureOption(options, "api_secret")
};
}
options.content_type = 'json';

return execute_request(method, params, auth, api_url, callback, options);
}

module.exports = {
call_generate_api
};
38 changes: 38 additions & 0 deletions lib/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const { URL } = require('url');
const Cache = require('./cache');
const utils = require("./utils");
const UploadStream = require('./upload_stream');
const { call_generate_api } = require('./api_client/call_generate_api');
const config = require("./config");
const ensureOption = require('./utils/ensureOption').defaults(config());

Expand Down Expand Up @@ -58,6 +59,43 @@ exports.upload = function upload(file, callback, options = {}) {
});
};

/**
* Generates an image from a text prompt and uploads the generated image to your product environment.
*
* This is a convenience wrapper that performs two steps: it first calls the image generation API with the
* provided generation parameters, and then uploads the generated image - identified by the `secure_url`
* returned by the generation API - by calling {@link upload}. The value resolved by the returned promise (and
* passed to the callback) is the result of the upload, not of the generation request. Any options are forwarded
* as-is to the upload step.
*
* @param {Object} generate_params Parameters for the generation request.
* @param {String} generate_params.prompt The text description of the image to generate (required).
* @param {String} [generate_params.model_family] The model family to use.
* @param {String} [generate_params.quality_tier] The quality tier to use within the model family.
* @param {String} [generate_params.model_id] A specific model identifier, overriding model_family/quality_tier.
* @param {Number} [generate_params.width] Desired image width in pixels.
* @param {Number} [generate_params.height] Desired image height in pixels.
* @param {Number} [generate_params.seed] Seed for reproducible generation.
* @param {Function} callback Callback function, invoked with the upload result.
* @param {Object} options Configuration options forwarded to the upload step
* (for example upload_preset, tags, folder).
*
* @return {Promise} A promise resolving with the upload result of the generated image.
*/
exports.generate = function generate(generate_params, callback, options = {}) {
return call_generate_api("POST", ["generate", "image"], generate_params, undefined, options)
.then((result) => {
const assets = result && result.data && result.data.assets;
const secure_url = Array.isArray(assets) && assets.length ? assets[0].secure_url : undefined;
return exports.upload(secure_url, callback, options);
}, (error) => {
if (typeof callback === "function") {
callback(error && error.error != null ? error : { error });
}
return Promise.reject(error);
});
};

exports.upload_large = function upload_large(path, callback, options = {}) {
if ((path != null) && isRemoteUrl(path)) {
// upload a remote file
Expand Down
11 changes: 11 additions & 0 deletions lib/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -1064,6 +1064,16 @@ function base_api_url(api_version) {
};
}

function base_processing_api_url() {
return (path = [], options = {}) => {
let cloudinary = ensureOption(options, "upload_prefix", UPLOAD_PREFIX);
let cloud_name = ensureOption(options, "cloud_name");
let encode_path = unencoded_path => encodeURIComponent(unencoded_path).replace("'", '%27');
let encoded_path = Array.isArray(path) ? path.map(encode_path) : encode_path(path);
return [cloudinary, 'v2', 'processing', cloud_name].concat(encoded_path).join("/");
};
}

function api_url(action = 'upload', options = {}) {
let resource_type = options.resource_type || "image";
return base_api_url_v1_1()([resource_type, action], options);
Expand Down Expand Up @@ -1724,6 +1734,7 @@ exports.jsonArrayParam = jsonArrayParam;
exports.download_folder = download_folder;
exports.base_api_url_v1 = base_api_url_v1_1;
exports.base_api_url_v2 = base_api_url_v2;
exports.base_processing_api_url = base_processing_api_url;
exports.download_backedup_asset = download_backedup_asset;
exports.compute_hash = compute_hash;
exports.build_distribution_domain = build_distribution_domain;
Expand Down
1 change: 1 addition & 0 deletions lib/v2/uploader.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ v1_adapters(exports, uploader, {
upload_stream: 0,
unsigned_upload: 2,
upload: 1,
generate: 1,
upload_large_part: 0,
upload_large: 1,
upload_chunked: 1,
Expand Down
168 changes: 168 additions & 0 deletions test/integration/api/uploader/generate_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
const expect = require('expect.js');
const sinon = require('sinon');
const https = require('https');
const { EventEmitter } = require('events');
const cloudinary = require('../../../../cloudinary');
const uploader = require('../../../../lib/uploader');
const createTestConfig = require('../../../testUtils/createTestConfig');

const CLOUD_NAME = 'test-cloud';
const SECURE_URL = 'https://res.cloudinary.com/test-cloud/image/upload/generated.png';

describe('uploader generate', function () {
let requestStub;
let uploadStub;
let capturedOptions;
let capturedBody;

beforeEach(function () {
cloudinary.config(createTestConfig({
cloud_name: CLOUD_NAME,
api_key: 'test-key',
api_secret: 'test-secret'
}));
capturedOptions = null;
capturedBody = null;
});

afterEach(function () {
if (requestStub && requestStub.restore) {
requestStub.restore();
}
if (uploadStub && uploadStub.restore) {
uploadStub.restore();
}
requestStub = null;
uploadStub = null;
});

// Stub https.request to emit a JSON response with the given status code and body.
function stubGenerateRequest(statusCode, responseBody) {
const mockResponse = new EventEmitter();
mockResponse.statusCode = statusCode;
mockResponse.headers = {};

requestStub = sinon.stub(https, 'request').callsFake(function (options, callback) {
capturedOptions = options;
setTimeout(() => callback(mockResponse), 0);

const mockRequest = new EventEmitter();
mockRequest.write = sinon.stub().callsFake((data) => {
capturedBody = data;
});
mockRequest.end = function () {
setTimeout(() => {
mockResponse.emit('data', JSON.stringify(responseBody));
mockResponse.emit('end');
}, 10);
};
mockRequest.setTimeout = sinon.stub();

return mockRequest;
});
}

function generateSuccessBody(secure_url = SECURE_URL) {
return {
data: {
assets: [
{
secure_url,
format: 'png',
width: 1024,
height: 768,
bytes: 2048576,
model: { family: 'flux', tier: 'premium', model_id: 'flux-2-pro' },
created_at: '2026-04-21T14:30:00Z'
}
]
},
request_id: 'test-request-id'
};
}

it('should call the generate endpoint with the generation params as a JSON body', function () {
stubGenerateRequest(200, generateSuccessBody());
// Prevent the upload step from issuing a real request.
uploadStub = sinon.stub(uploader, 'upload').resolves({ secure_url: SECURE_URL });

return cloudinary.v2.uploader.generate({ prompt: 'A man with a hat', model_family: 'flux' }).then(() => {
sinon.assert.calledWith(requestStub, sinon.match({
pathname: sinon.match(new RegExp(`/v2/processing/${CLOUD_NAME}/generate/image`)),
method: sinon.match('POST')
}));
expect(capturedOptions.headers['Content-Type']).to.eql('application/json');
const body = JSON.parse(capturedBody);
expect(body.prompt).to.eql('A man with a hat');
expect(body.model_family).to.eql('flux');
});
});

it('should upload the generated image and resolve with the upload result', function () {
stubGenerateRequest(200, generateSuccessBody());
const uploadResult = { public_id: 'generated', secure_url: SECURE_URL };
uploadStub = sinon.stub(uploader, 'upload').resolves(uploadResult);

const options = { upload_preset: 'my_preset', tags: ['generated'] };
return cloudinary.v2.uploader.generate({ prompt: 'A man with a hat' }, options).then((result) => {
sinon.assert.calledWith(uploadStub, SECURE_URL);
// Options are forwarded to the upload step.
const forwardedOptions = uploadStub.firstCall.args[2];
expect(forwardedOptions.upload_preset).to.eql('my_preset');
expect(result).to.eql(uploadResult);
});
});

it('should forward the callback to the upload step on success', function (done) {
stubGenerateRequest(200, generateSuccessBody());
const uploadResult = { public_id: 'generated', secure_url: SECURE_URL };
// Mimic the real upload by invoking the callback it receives.
uploadStub = sinon.stub(uploader, 'upload').callsFake((file, callback) => {
if (typeof callback === 'function') {
callback(uploadResult);
}
return Promise.resolve(uploadResult);
});

cloudinary.v2.uploader.generate({ prompt: 'A man with a hat' }, function (error, result) {
try {
expect(error).to.be(undefined);
expect(result).to.eql(uploadResult);
done();
} catch (e) {
done(e);
}
});
});

it('should not call upload and should reject when generation fails', function () {
stubGenerateRequest(400, { error: { message: 'missing parameters' } });
uploadStub = sinon.stub(uploader, 'upload').resolves({});

return cloudinary.v2.uploader.generate({ prompt: '' }).then(() => {
throw new Error('Expected generate to reject');
}, (error) => {
sinon.assert.notCalled(uploadStub);
expect(error).to.be.ok();
});
});

it('should invoke the callback with the error when generation fails', function (done) {
stubGenerateRequest(400, { error: { message: 'missing parameters' } });
uploadStub = sinon.stub(uploader, 'upload').resolves({});

cloudinary.v2.uploader.generate({ prompt: '' }, function (error, result) {
try {
expect(error).to.be.ok();
expect(error.message).to.eql('missing parameters');
expect(result).to.be(undefined);
sinon.assert.notCalled(uploadStub);
done();
} catch (e) {
done(e);
}
}).catch(() => {
// Swallow the rejected promise; assertions are made in the callback.
});
});
});
23 changes: 23 additions & 0 deletions types/cloudinary_ts_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,29 @@ cloudinary.v2.uploader.upload("ftp://user1:mypass@ftp.example.com/sample.jpg",
console.log(result, error);
});

// $ExpectType Promise<UploadApiResponse>
cloudinary.v2.uploader.generate({prompt: "A man with a hat"},
function (error, result) {
console.log(result, error);
});

// $ExpectType Promise<UploadApiResponse>
cloudinary.v2.uploader.generate(
{
prompt: "A photorealistic sunset over a mountain lake",
model_family: "flux",
quality_tier: "premium",
width: 1024,
height: 768
},
{
upload_preset: "my_preset",
tags: ["generated"]
},
function (error, result) {
console.log(result, error);
});

// $ExpectType UploadStream | Promise<UploadApiResponse>
cloudinary.v2.uploader.upload_large("my_large_video.mp4",
{
Expand Down
23 changes: 23 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,25 @@ declare module 'cloudinary' {
[futureKey: string]: any;
}

export interface GenerateImageOptions {
/** The text description of the image to generate. */
prompt: string;
/** The model family to use. */
model_family?: string;
/** The quality tier to use within the model family. */
quality_tier?: string;
/** A specific model identifier, overriding model_family/quality_tier. */
model_id?: string;
/** Desired image width in pixels. */
width?: number;
/** Desired image height in pixels. */
height?: number;
/** Seed for reproducible generation. */
seed?: number;

[futureKey: string]: any;
}

export interface UploadApiOptions {
access_mode?: AccessMode;
allowed_formats?: Array<VideoFormat> | Array<ImageFormat>;
Expand Down Expand Up @@ -1406,6 +1425,10 @@ declare module 'cloudinary' {

function upload(file: string, callback?: UploadResponseCallback): Promise<UploadApiResponse>;

function generate(params: GenerateImageOptions, options?: UploadApiOptions, callback?: UploadResponseCallback): Promise<UploadApiResponse>;

function generate(params: GenerateImageOptions, callback?: UploadResponseCallback): Promise<UploadApiResponse>;

function upload_chunked(path: string, options?: UploadApiOptions, callback?: UploadResponseCallback): UploadStream;

function upload_chunked(path: string, callback?: UploadResponseCallback): UploadStream;
Expand Down
Loading