Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ console.log('Expires in:', result.imageUrlExpiresIn);
|--------|--------|-------------|
| `generate` | `client.generate` | Create SVGs from text prompts |
| `edit` | `client.edit` | Modify existing images/SVGs with AI |
| `removeBackground` | `client.removeBackground` | Remove an image's background and return an SVG |
| `convert.aiVectorize` | `client.convert.aiVectorize` | AI-powered raster to SVG conversion |
| `convert.trace` | `client.convert.trace` | Algorithmic raster to SVG tracing |
| `convert.svgToVector` | `client.convert.svgToVector` | SVG to vector formats (PDF, EPS, DXF, AI, PS) |
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@genwave/svgmaker-sdk",
"version": "1.1.0",
"version": "1.2.0",
"description": "Official Node.js SDK for SVGMaker API",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand Down
15 changes: 14 additions & 1 deletion src/clients/BaseClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@ export abstract class BaseClient {
return mimeTypes[ext] || 'application/octet-stream';
}

/**
* Build the authentication header for raw fetch calls (form-data and streaming
* paths that bypass HttpClient.buildHeaders). Prefers the OAuth Bearer access
* token when present; otherwise falls back to the `x-api-key` header. Mirrors
* the logic in HttpClient so all transports authenticate consistently.
*/
protected buildAuthHeaders(): Record<string, string> {
if (this.config.accessToken) {
return { Authorization: `Bearer ${this.config.accessToken}` };
}
return { 'x-api-key': this.config.apiKey };
}

/**
* Execute a POST request with FormData body and unwrap the v1 API envelope
* @param endpoint API endpoint path (e.g., '/v1/convert/trace')
Expand All @@ -183,7 +196,7 @@ export abstract class BaseClient {
const response = await fetch(`${this.config.baseUrl}${endpoint}`, {
method: 'POST',
headers: {
'x-api-key': this.config.apiKey,
...this.buildAuthHeaders(),
},
body: formData,
});
Expand Down
2 changes: 1 addition & 1 deletion src/clients/GalleryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class GalleryClient extends BaseClient {
method: 'GET',
headers: {
Accept: 'application/json',
'x-api-key': this.config.apiKey,
...this.buildAuthHeaders(),
},
});

Expand Down
2 changes: 1 addition & 1 deletion src/clients/GenerationsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ export class GenerationsClient extends BaseClient {
method: 'GET',
headers: {
Accept: 'application/json',
'x-api-key': this.config.apiKey,
...this.buildAuthHeaders(),
},
});

Expand Down
249 changes: 249 additions & 0 deletions src/clients/RemoveBackgroundClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import { BaseClient } from './BaseClient';
import {
RemoveBackgroundParams,
RemoveBackgroundResponse,
RemoveBackgroundStreamEvent,
} from '../types/api';
import { SVGMakerClient } from '../core/SVGMakerClient';
import { z } from 'zod';
import { Readable } from 'stream';
import { decodeSvgContent } from '../utils/base64';

/**
* Schema for validating remove background parameters
*/
const removeBackgroundParamsSchema = z.object({
file: z.union([z.string(), z.instanceof(Buffer), z.instanceof(Readable)]),
stream: z.boolean().optional(),
svgText: z.boolean().optional(),
storage: z.boolean().optional(),
});

/**
* Client for the Remove Background API
*
* Removes the background from an image and returns the result as an SVG with
* transparency. Accepts any raster image (PNG, JPEG, WebP, etc.) or SVG.
*/
export class RemoveBackgroundClient extends BaseClient {
private params: Partial<RemoveBackgroundParams> = {};

/**
* Create a new Remove Background client
* @param client Parent SVGMaker client
*/
constructor(client: SVGMakerClient) {
super(client);
}

/**
* Execute the Remove Background request
* @returns Remove Background response
*/
public async execute(): Promise<RemoveBackgroundResponse> {
this.logger.debug('Starting background removal', {
hasFile: !!this.params.file,
svgTextRequested: !!this.params.svgText,
});

// Validate parameters
this.validateRequest(this.params, removeBackgroundParamsSchema);

// Prepare form data
const formData = new FormData();

// Add file
await this.addFileToForm(formData, 'file', this.params.file!);

// Add optional parameters
this.appendOptionalParams(formData, this.params as Record<string, any>, [
'storage',
'stream',
'svgText',
]);

// Execute request
const { data, metadata: responseMetadata } = await this.executeFormDataRequest<any>(
'/v1/remove-background',
formData
);

this.logger.debug('Background removal completed', {
creditCost: data.creditCost,
hasSvgText: !!data.svgText,
});

// Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too)
let svgText: string | undefined = undefined;
if (data.svgText && typeof data.svgText === 'string') {
svgText = decodeSvgContent(data.svgText);
}

return {
svgUrl: data.svgUrl,
creditCost: data.creditCost,
message: data.message ?? '',
svgUrlExpiresIn: data.svgUrlExpiresIn,
generationId: data.generationId,
metadata: responseMetadata,
svgText,
} as RemoveBackgroundResponse;
}

/**
* Configure the remove background parameters
* @param config Configuration object with remove background parameters
* @returns New client instance
*/
public configure(config: Partial<RemoveBackgroundParams>): RemoveBackgroundClient {
this.logger.debug('Configuring remove background parameters', { config });

const client = this.clone();
client.params = { ...client.params, ...config };
return client;
}

/**
* Stream the remove background response
* @returns Readable stream of events
*/
public stream(): Readable {
// Create a clone with streaming enabled
const client = this.clone();
client.params.stream = true;

// Validate parameters
this.validateRequest(client.params, removeBackgroundParamsSchema);

// Create a readable stream for the events
const stream = new Readable({
objectMode: true,
read() {},
});

// Execute the request and handle streaming
(async () => {
try {
// Prepare form data
const formData = new FormData();

// Add file
await this.addFileToForm(formData, 'file', client.params.file!);

// Add storage option if present
if (client.params.storage !== undefined) {
formData.append('storage', String(client.params.storage));
}

// Add stream option
formData.append('stream', 'true');

if (client.params.svgText) {
formData.append('svgText', String(client.params.svgText));
}

// Make request to the streaming endpoint using native fetch. Auth headers
// prefer the OAuth Bearer token when present, else the x-api-key.
const response = await fetch(`${this.config.baseUrl}/v1/remove-background`, {
method: 'POST',
headers: {
Accept: 'text/event-stream',
...this.buildAuthHeaders(),
},
body: formData,
});

if (!response.ok) {
await this.handleFetchErrorResponse(response);
}

const reader = response.body?.getReader();
if (!reader) {
throw new Error('Response body is not readable');
}

const decoder = new TextDecoder();
let buffer = '';
const accumulated: Record<string, unknown> = {};

while (true) {
const { done, value } = await reader.read();
if (done) break;

// Decode the chunk and add to buffer
buffer += decoder.decode(value, { stream: true });

// Split by newlines to get individual JSON objects
const lines = buffer.split('\n');

// Keep the last incomplete line in the buffer
buffer = lines.pop() || '';

// Process each complete line
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '') continue;

try {
// Parse the JSON chunk directly (no "data:" prefix like SSE)
const event = JSON.parse(trimmedLine) as RemoveBackgroundStreamEvent;
// Normalize svgText (API now sends raw SVG text, but we handle legacy base64 too)
if (event.svgText && typeof event.svgText === 'string') {
event.svgText = decodeSvgContent(event.svgText);
}
// Accumulate fields from all events
for (const [key, value] of Object.entries(event)) {
if (value !== undefined && key !== 'status' && key !== 'message') {
accumulated[key] = value;
}
}

// When complete, merge accumulated fields into the event
if (event.status === 'complete' || event.status === 'error') {
const mergedEvent = { ...accumulated, ...event };
stream.push(mergedEvent);
stream.push(null);
return;
}

stream.push(event);
} catch (e) {
console.error('Error parsing streaming chunk:', e);
console.error('Problematic line:', trimmedLine);
}
}
}

// Process any remaining data in buffer
if (buffer.trim()) {
try {
const event = JSON.parse(buffer.trim()) as RemoveBackgroundStreamEvent;
stream.push(event);
} catch (e) {
console.error('Error parsing final chunk:', e);
}
}

// End of stream
stream.push(null);
} catch (error) {
console.error('Streaming error:', error);
stream.emit('error', error);
stream.push(null);
}
})();

return stream;
}

/**
* Create a clone of this client
* @returns New client instance
*/
protected clone(): RemoveBackgroundClient {
const client = new RemoveBackgroundClient(this.client);
this.copyTo(client);
client.params = { ...this.params };
return client;
}
}
15 changes: 12 additions & 3 deletions src/core/SVGMakerClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GenerationsClient } from '../clients/GenerationsClient';
import { GalleryClient } from '../clients/GalleryClient';
import { AccountClient } from '../clients/AccountClient';
import { OptimizeSvgClient } from '../clients/OptimizeSvgClient';
import { RemoveBackgroundClient } from '../clients/RemoveBackgroundClient';
import { createRetryWrapper } from '../utils/retry';
import { createRateLimiter } from '../utils/rateLimit';
import { Logger, createLogger } from '../utils/logger';
Expand Down Expand Up @@ -66,6 +67,11 @@ export class SVGMakerClient {
*/
public readonly edit: EditClient;

/**
* Remove Background client — removes an image's background and returns an SVG
*/
public readonly removeBackground: RemoveBackgroundClient;

/**
* Convert namespace — contains AI vectorize and future conversion clients
*/
Expand Down Expand Up @@ -113,11 +119,13 @@ export class SVGMakerClient {
* @param config Additional configuration options
*/
constructor(apiKey: string, config: Partial<SVGMakerConfig> = {}) {
if (!apiKey) {
throw new ValidationError('API key is required');
if (!apiKey && !config.accessToken) {
throw new ValidationError('Either an API key or an access token is required');
}

// Merge default config with provided config
// Merge default config with provided config.
// The explicit `apiKey` argument wins over any `apiKey` in `config`,
// while an OAuth `accessToken` from `config` is preserved (different key).
this.config = {
...DEFAULT_CONFIG,
...config,
Expand All @@ -142,6 +150,7 @@ export class SVGMakerClient {
// Create API clients
this.generate = new GenerateClient(this);
this.edit = new EditClient(this);
this.removeBackground = new RemoveBackgroundClient(this);
this.convert = {
aiVectorize: new AIVectorizeClient(this),
trace: new TraceClient(this),
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { SvgToVectorClient } from './clients/convert/SvgToVectorClient';
import { RasterToRasterClient } from './clients/convert/RasterToRasterClient';
import { BatchConvertClient } from './clients/convert/BatchConvertClient';
import { EnhancePromptClient } from './clients/EnhancePromptClient';
import { RemoveBackgroundClient } from './clients/RemoveBackgroundClient';

// Export error classes
import * as Errors from './errors/CustomErrors';
Expand Down Expand Up @@ -44,6 +45,7 @@ export {
RasterToRasterClient,
BatchConvertClient,
EnhancePromptClient,
RemoveBackgroundClient,

// Utils
HttpClient,
Expand Down
Loading
Loading