diff --git a/README.md b/README.md index f8098a0..8ff43eb 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ v1.0.0 includes breaking changes. If you're upgrading from v0.x, see the **[Migr - **TypeScript Native**: Full type safety with comprehensive type definitions - **Automatic Retries**: Built-in retry logic with exponential backoff - **Streaming Support**: Real-time progress updates via Server-Sent Events +- **Raster Mode**: Return PNG output directly on generate and edit, skipping vectorization - **Input Validation**: Zod-based schema validation for all requests - **Dual Package**: ESM and CommonJS support @@ -76,6 +77,28 @@ for await (const event of stream) { } ``` +### Raster Mode + +Pass `raster: true` to `generate` or `edit` to receive a PNG instead of an SVG. This skips vectorization, so it's faster and simpler. In raster mode the response has no `svgUrl`; instead you get `imageUrl` (a temporary URL to the PNG) and `imageUrlExpiresIn`. + +```typescript +// Generate a raster PNG instead of an SVG +const result = await client.generate + .configure({ + prompt: 'A minimalist mountain landscape', + quality: 'high', + raster: true, + }) + .execute(); + +console.log('Image URL:', result.imageUrl); +console.log('Expires in:', result.imageUrlExpiresIn); + +// raster: true also works on client.edit +``` + +> **Note:** `raster: true` cannot be combined with `storage: true` — raster mode returns temporary URLs only, and the SDK throws if both are set. Set `base64Png: true` if you need the PNG as a Buffer via `result.pngImageData`. + ## Available Clients | Client | Access | Description | diff --git a/docs/sdk-documentation.md b/docs/sdk-documentation.md index 46838a2..a4e2471 100644 --- a/docs/sdk-documentation.md +++ b/docs/sdk-documentation.md @@ -182,19 +182,22 @@ if (result.svgText) { | `styleParams` | `StyleParams` | `{}` | Style parameters object | | `base64Png` | `boolean` | `false` | Include PNG preview in response | | `svgText` | `boolean` | `false` | Include SVG source code in response | +| `raster` | `boolean` | `false` | Return a raster PNG instead of an SVG, skipping vectorization. The response then contains `imageUrl`/`imageUrlExpiresIn` instead of `svgUrl`, and `svgText` is not produced. Cannot be combined with `storage: true` (the SDK throws a validation error if both are set) | #### Generation Response Fields | Field | Type | Description | |-------|------|-------------| -| `svgUrl` | `string` | URL of the generated SVG | +| `svgUrl` | `string` | URL of the generated SVG. Absent when `raster: true` | +| `imageUrl` | `string \| undefined` | URL of the generated raster PNG (only when `raster: true`; temporary) | +| `imageUrlExpiresIn` | `string \| undefined` | Expiration for the raster PNG URL (only when `raster: true`) | | `creditCost` | `number` | Credits consumed by the request | | `quality` | `string` | Quality level used | | `message` | `string` | API response message | | `svgUrlExpiresIn` | `number` | Seconds until the SVG URL expires | | `generationId` | `string` | Unique ID for the generation | -| `pngImageData` | `Buffer \| undefined` | Decoded PNG preview (when `base64Png` is true) | -| `svgText` | `string \| undefined` | SVG source code (when `svgText` is true) | +| `pngImageData` | `Buffer \| undefined` | Decoded PNG preview (when `base64Png` is true; also available in raster mode) | +| `svgText` | `string \| undefined` | SVG source code (when `svgText` is true; not produced when `raster: true`) | | `revisedPrompt` | `string \| undefined` | Prompt as revised by the model | | `metadata` | `object` | Additional API metadata | @@ -253,8 +256,9 @@ const result2 = await client.edit | `background` | `'auto' \| 'transparent' \| 'opaque'` | `'auto'` | Background handling | | `base64Png` | `boolean` | `false` | Include PNG preview in response | | `svgText` | `boolean` | `false` | Include SVG source code in response | +| `raster` | `boolean` | `false` | Return a raster PNG instead of an SVG, skipping vectorization. The response then contains `imageUrl`/`imageUrlExpiresIn` instead of `svgUrl`, and `svgText` is not produced. Cannot be combined with `storage: true` (the SDK throws a validation error if both are set) | -The response shape is identical to the [Generation Response Fields](#generation-response-fields) table above. +The response shape is identical to the [Generation Response Fields](#generation-response-fields) table above. When `raster: true`, the `svgUrl` field is absent and the response instead exposes `imageUrl` and `imageUrlExpiresIn` (the temporary raster PNG URL and its expiration); `pngImageData` remains available if `base64Png: true` is set. ### Convert Namespace diff --git a/docs/v1-api-documentation.md b/docs/v1-api-documentation.md index 3e0159a..7e0c513 100644 --- a/docs/v1-api-documentation.md +++ b/docs/v1-api-documentation.md @@ -154,6 +154,7 @@ x-api-key: svgmaker-io{your-api-key} | `aspectRatio` | string | No | `auto` | Aspect ratio: `auto`, `portrait`, `landscape`, or `square` | | `background` | string | No | `auto` | Background type: `auto`, `transparent`, or `opaque` | | `storage` | boolean | No | `false` | Whether to save files to cloud storage. When `false`, returns proxy URLs immediately (files not persisted). When `true`, saves files to Firebase Storage. `generationId` is always returned. | +| `raster` | boolean | No | `false` | When `true`, returns a raster PNG instead of an SVG (vectorization is skipped). The response includes `imageUrl` and `imageUrlExpiresIn` instead of `svgUrl`. Cannot be combined with `storage: true` — raster mode returns temporary URLs only. | | `stream` | boolean | No | `false` | Enable streaming response (Server-Sent Events) | | `base64Png` | boolean | No | `false` | Include base64-encoded PNG preview in response | | `svgText` | boolean | No | `false` | Include raw SVG source code as text in response | @@ -255,6 +256,29 @@ curl -X POST https://api.svgmaker.io/v1/generate \ } ``` +#### Response (raster: true) + +When `raster: true`, the endpoint returns a raster PNG instead of an SVG. The `svgUrl` field is absent; the response includes `imageUrl` and `imageUrlExpiresIn` instead. `raster: true` cannot be combined with `storage: true`. + +```json +{ + "success": true, + "data": { + "creditCost": 3, + "quality": "high", + "message": "Image generated successfully", + "imageUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "imageUrlExpiresIn": "12h", + "base64Png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 3, + "creditsRemaining": 102.5 + } +} +``` + #### Response Fields | Field | Type | Description | @@ -262,8 +286,10 @@ curl -X POST https://api.svgmaker.io/v1/generate \ | `creditCost` | number | Credits charged for this operation | | `quality` | string | Quality level used (`low`, `medium`, `high`) | | `message` | string | Status message (`Image generated successfully` when `storage: false`, `Files stored to cloud successfully` when `storage: true`) | -| `svgUrl` | string | URL to access the generated SVG (proxy URL or persistent URL) | +| `svgUrl` | string | URL to access the generated SVG (proxy URL or persistent URL). Absent when `raster: true`. | | `svgUrlExpiresIn` | string | Expiration time for proxy URLs (e.g., "12h"). Only present for proxy URLs. | +| `imageUrl` | string | URL to access the generated raster PNG (temporary). Only present when `raster: true`. | +| `imageUrlExpiresIn` | string | Expiration time for the raster PNG URL (e.g., "12h"). Only present when `raster: true`. | | `generationId` | string | Unique identifier for the generation. Always present. | | `base64Png` | string | Base64-encoded PNG preview. Only present when `base64Png: true`. | | `svgText` | string | Raw SVG source code. Only present when `svgText: true`. | @@ -297,6 +323,7 @@ x-api-key: svgmaker-io{your-api-key} | `aspectRatio` | string | No | `auto` | Aspect ratio: `auto`, `portrait`, `landscape`, or `square` | | `background` | string | No | `auto` | Background type: `auto`, `transparent`, or `opaque` | | `storage` | string | No | `false` | Whether to save to cloud storage. Pass `"true"` or `"false"` as a string. | +| `raster` | string | No | `false` | When `"true"`, returns a raster PNG instead of an SVG (vectorization is skipped). The response includes `imageUrl` and `imageUrlExpiresIn` instead of `svgUrl`. Cannot be combined with `storage: true` — raster mode returns temporary URLs only. Pass `"true"` or `"false"` as a string. | | `stream` | string | No | `false` | Enable streaming response. Pass `"true"` or `"false"` as a string. | | `base64Png` | string | No | `false` | Include base64-encoded PNG preview. Pass `"true"` or `"false"` as a string. | | `svgText` | string | No | `false` | Include raw SVG source code. Pass `"true"` or `"false"` as a string. | @@ -378,9 +405,32 @@ curl -X POST https://api.svgmaker.io/v1/edit \ } ``` +#### Response (raster: true) + +When `raster: true`, the endpoint returns a raster PNG instead of an SVG. The `svgUrl` field is absent; the response includes `imageUrl` and `imageUrlExpiresIn` instead. `raster: true` cannot be combined with `storage: true`. + +```json +{ + "success": true, + "data": { + "creditCost": 3, + "quality": "medium", + "message": "Image edited successfully", + "imageUrl": "https://svgmaker.io/api/files/eyJ1cmwiOiJodHRwOi8v...", + "imageUrlExpiresIn": "12h", + "base64Png": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..." + }, + "metadata": { + "requestId": "req_6-vXlRW6lfD_", + "creditsUsed": 3, + "creditsRemaining": 97.5 + } +} +``` + #### Response Fields -Same as Generate endpoint (see above). +Same as Generate endpoint (see above). When `raster: true`, the response includes `imageUrl` and `imageUrlExpiresIn` instead of `svgUrl`. --- diff --git a/package-lock.json b/package-lock.json index cddee5b..8b4a6ef 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@genwave/svgmaker-sdk", - "version": "1.0.0-rc.1", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@genwave/svgmaker-sdk", - "version": "1.0.0-rc.1", + "version": "1.1.0", "license": "MIT", "dependencies": { "async-retry": "^1.3.3", diff --git a/package.json b/package.json index 3aa02c8..dcaa0d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@genwave/svgmaker-sdk", - "version": "1.0.0", + "version": "1.1.0", "description": "Official Node.js SDK for SVGMaker API", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.js", diff --git a/src/clients/EditClient.ts b/src/clients/EditClient.ts index a26d892..034c92b 100644 --- a/src/clients/EditClient.ts +++ b/src/clients/EditClient.ts @@ -42,12 +42,17 @@ const editParamsSchema = z base64Png: z.boolean().optional(), svgText: z.boolean().optional(), model: z.string().optional(), + raster: z.boolean().optional(), }) .refine(data => data.prompt || data.styleParams, { message: 'Either prompt or styleParams must be provided', }) .refine(data => !(data.model && data.quality), { message: "Cannot specify both 'model' and 'quality'. Use one or the other.", + }) + .refine(data => !(data.raster && data.storage), { + message: + "Cannot use 'storage: true' with 'raster: true'. Raster mode returns temporary URLs only.", }); /** @@ -103,6 +108,7 @@ export class EditClient extends BaseClient { 'base64Png', 'svgText', 'model', + 'raster', ]); // Execute request @@ -131,6 +137,8 @@ export class EditClient extends BaseClient { pngImageData, svgText, quality: data.quality, + imageUrl: data.imageUrl, + imageUrlExpiresIn: data.imageUrlExpiresIn, }; this.logger.debug('Image/SVG edit completed', { @@ -241,6 +249,10 @@ export class EditClient extends BaseClient { formData.append('model', client.params.model); } + if (client.params.raster !== undefined) { + formData.append('raster', String(client.params.raster)); + } + // Make request to the streaming endpoint using native fetch const response = await fetch(`${this.config.baseUrl}/v1/edit`, { method: 'POST', diff --git a/src/clients/GenerateClient.ts b/src/clients/GenerateClient.ts index 38da1fd..95e9f5a 100644 --- a/src/clients/GenerateClient.ts +++ b/src/clients/GenerateClient.ts @@ -19,6 +19,7 @@ const generateParamsSchema = z base64Png: z.boolean().optional(), svgText: z.boolean().optional(), model: z.string().optional(), + raster: z.boolean().optional(), styleParams: z .object({ style: z @@ -44,6 +45,10 @@ const generateParamsSchema = z }) .refine(data => !(data.model && data.quality), { message: "Cannot specify both 'model' and 'quality'. Use one or the other.", + }) + .refine(data => !(data.raster && data.storage), { + message: + "Cannot use 'storage: true' with 'raster: true'. Raster mode returns temporary URLs only.", }); /** @@ -105,6 +110,8 @@ export class GenerateClient extends BaseClient { pngImageData: rawResult.pngImageData, svgText, quality: rawResult.quality ?? this.params.quality ?? 'medium', + imageUrl: rawResult.imageUrl, + imageUrlExpiresIn: rawResult.imageUrlExpiresIn, }; this.logger.debug('SVG generation completed', { diff --git a/src/types/api.ts b/src/types/api.ts index 97f8aab..8bcd989 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -92,6 +92,9 @@ export interface GenerateParams { /** Optional: Specific AI model ID to use. Cannot be combined with quality. Credits are charged based on the model. */ model?: string; + + /** Optional: Return PNG instead of SVG. Skips vectorization. Cannot combine with storage: true. */ + raster?: boolean; } /** @@ -130,6 +133,9 @@ export interface EditParams { /** Optional: Specific AI model ID to use. Cannot be combined with quality. Credits are charged based on the model. */ model?: string; + + /** Optional: Return PNG instead of SVG. Skips vectorization. Cannot combine with storage: true. */ + raster?: boolean; } /** @@ -159,8 +165,8 @@ export type ConvertParams = AiVectorizeParams; * Base SVGMaker API response */ export interface BaseResponse { - /** URL to the generated/edited/converted SVG */ - svgUrl: string; + /** URL to the generated/edited/converted SVG (absent in raster mode) */ + svgUrl?: string; /** Number of credits consumed by the operation */ creditCost: number; @@ -176,6 +182,12 @@ export interface BaseResponse { /** Unique identifier for this generation */ generationId?: string; + + /** URL to the raster PNG image (only when raster: true) */ + imageUrl?: string; + + /** Expiration time for the image URL (only when raster: true) */ + imageUrlExpiresIn?: string; } /** @@ -378,8 +390,8 @@ export interface ProcessingStreamEvent extends StreamEventBase { */ export interface CompleteStreamEvent extends StreamEventBase { status: 'complete'; - /** URL to the generated/edited/converted SVG */ - svgUrl: string; + /** URL to the generated/edited/converted SVG (absent in raster mode) */ + svgUrl?: string; /** SVG source code as text - only when svgText=true */ svgText?: string; /** Expiration time for the SVG URL */ @@ -392,6 +404,10 @@ export interface CompleteStreamEvent extends StreamEventBase { message?: string; /** Response metadata */ metadata?: ResponseMetadata; + /** URL to the raster PNG image (only when raster: true) */ + imageUrl?: string; + /** Expiration time for the image URL (only when raster: true) */ + imageUrlExpiresIn?: string; } /** diff --git a/tests/manual-test.ts b/tests/manual-test.ts index ee04768..ad1ac02 100644 --- a/tests/manual-test.ts +++ b/tests/manual-test.ts @@ -250,6 +250,50 @@ async function generateWithStyleParams(): Promise { } } +// ========== GENERATE RASTER TESTS ========== + +async function generateRasterOnly(): Promise { + console.log('\n[Generate] Raster mode (raster: true)'); + try { + const result = await client.generate + .configure({ + prompt: 'A simple red circle', + quality: 'low', + raster: true, + }) + .execute(); + console.log(' PASS'); + console.log(` Image URL: ${result.imageUrl}`); + console.log(` Image URL expires in: ${result.imageUrlExpiresIn}`); + console.log(` Credits: ${result.creditCost}`); + if (result.generationId) console.log(` Generation ID: ${result.generationId}`); + if (result.metadata) console.log(` Credits remaining: ${result.metadata.creditsRemaining}`); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} + +async function generateRasterWithStorage(): Promise { + console.log('\n[Generate] Raster + storage (should fail validation)'); + try { + await client.generate + .configure({ + prompt: 'A blue square', + quality: 'low', + raster: true, + storage: true, + }) + .execute(); + console.log(' FAIL: Should have thrown validation error'); + return false; + } catch (error: any) { + console.log(` PASS (caught expected error): ${error.message}`); + return true; + } +} + // ========== EDIT TESTS ========== async function editWithModel(): Promise { @@ -429,6 +473,62 @@ async function editWithStyleParams(): Promise { } } +// ========== EDIT RASTER TESTS ========== + +async function editRasterOnly(): Promise { + console.log('\n[Edit] Raster mode (raster: true)'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + const result = await client.edit + .configure({ + image: img, + prompt: 'Add a red border', + quality: 'low', + raster: true, + }) + .execute(); + console.log(' PASS'); + console.log(` Image URL: ${result.imageUrl}`); + console.log(` Image URL expires in: ${result.imageUrlExpiresIn}`); + console.log(` Credits: ${result.creditCost}`); + if (result.generationId) console.log(` Generation ID: ${result.generationId}`); + if (result.metadata) console.log(` Credits remaining: ${result.metadata.creditsRemaining}`); + return true; + } catch (error: any) { + console.log(` FAIL: ${error.message}`); + return false; + } +} + +async function editRasterWithStorage(): Promise { + console.log('\n[Edit] Raster + storage (should fail validation)'); + const img = findTestImage(); + if (!img) { + console.log(' SKIP: No test image found'); + return false; + } + try { + await client.edit + .configure({ + image: img, + prompt: 'Make it green', + quality: 'low', + raster: true, + storage: true, + }) + .execute(); + console.log(' FAIL: Should have thrown validation error'); + return false; + } catch (error: any) { + console.log(` PASS (caught expected error): ${error.message}`); + return true; + } +} + // ========== CONVERT TESTS ========== async function convertBasic(): Promise { @@ -523,6 +623,13 @@ async function runTests(): Promise { results['edit-styleparams'] = await editWithStyleParams(); } + if (testType === 'raster' || testType === 'all') { + results['generate-raster'] = await generateRasterOnly(); + results['generate-raster-storage-error'] = await generateRasterWithStorage(); + results['edit-raster'] = await editRasterOnly(); + results['edit-raster-storage-error'] = await editRasterWithStorage(); + } + if (testType === 'convert' || testType === 'all') { results['convert-basic'] = await convertBasic(); results['convert-stream'] = await convertWithStream();