Skip to content

Commit

Permalink
IMAGES-1355: Implement draw functionality in Images binding
Browse files Browse the repository at this point in the history
  • Loading branch information
ns476 committed Oct 30, 2024
1 parent ea8a12f commit 5b8cd6e
Show file tree
Hide file tree
Showing 4 changed files with 266 additions and 32 deletions.
125 changes: 114 additions & 11 deletions src/cloudflare/internal/images-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ type Fetcher = {
fetch: typeof fetch;
};

type TargetedTransform = ImageTransform & {
imageIndex: number;
};

// Draw image drawImageIndex on image targetImageIndex
type DrawCommand = ImageDrawOptions & {
drawImageIndex: number;
targetImageIndex: number;
};

type RawInfoResponse =
| { format: 'image/svg+xml' }
| {
Expand Down Expand Up @@ -51,8 +61,15 @@ async function streamToBlob(stream: ReadableStream<Uint8Array>): Promise<Blob> {
return new Response(stream).blob();
}

class DrawTransformer {
public constructor(
public readonly child: ImageTransformerImpl,
public readonly options: ImageDrawOptions
) {}
}

class ImageTransformerImpl implements ImageTransformer {
private transforms: ImageTransform[];
private transforms: (ImageTransform | DrawTransformer)[];
private consumed: boolean;

public constructor(
Expand All @@ -68,19 +85,38 @@ class ImageTransformerImpl implements ImageTransformer {
return this;
}

public async output(
options: ImageOutputOptions
): Promise<ImageTransformationResult> {
if (this.consumed) {
throw new ImagesErrorImpl(
'IMAGES_TRANSFORM_ERROR 9525: ImageTransformer consumed; you may only call .output() once',
9525
public draw(
image: ReadableStream<Uint8Array> | ImageTransformer,
options?: ImageDrawOptions
): this {
if (isTransformer(image)) {
image.consume();
this.transforms.push(new DrawTransformer(image, options || {}));
} else {
this.transforms.push(
new DrawTransformer(
new ImageTransformerImpl(
this.fetcher,
image as ReadableStream<Uint8Array>
),
options || {}
)
);
}
this.consumed = true;

return this;
}

public async output(
options: ImageOutputOptions
): Promise<ImageTransformationResult> {
const body = new FormData();

this.consume();
body.append('image', await streamToBlob(this.stream));

await this.serializeTransforms(body);

body.append('output_format', options.format);
if (options.quality !== undefined) {
body.append('output_quality', options.quality.toString());
Expand All @@ -90,8 +126,6 @@ class ImageTransformerImpl implements ImageTransformer {
body.append('background', options.background);
}

body.append('transforms', JSON.stringify(this.transforms));

const response = await this.fetcher.fetch(
'https://js.images.cloudflare.com/transform',
{
Expand All @@ -104,6 +138,75 @@ class ImageTransformerImpl implements ImageTransformer {

return new TransformationResultImpl(response);
}

private consume(): void {
if (this.consumed) {
throw new ImagesErrorImpl(
'IMAGES_TRANSFORM_ERROR 9525: ImageTransformer consumed; you may only call .output() or draw a transformer once',
9525
);
}

this.consumed = true;
}

private async serializeTransforms(body: FormData): Promise<void> {
const transforms: (TargetedTransform | DrawCommand)[] = [];

// image 0 is the canvas, so the first draw_image has index 1
let drawImageIndex = 1;
function appendDrawImage(blob: Blob): number {
body.append('draw_image', blob);
return drawImageIndex++;
}

async function walkTransforms(
targetImageIndex: number,
imageTransforms: (ImageTransform | DrawTransformer)[]
): Promise<void> {
for (const transform of imageTransforms) {
if (!isDrawTransformer(transform)) {
// Simple transformation - we just have to tell the backend to run it
// against this image
transforms.push({
imageIndex: targetImageIndex,
...transform,
});
} else if (isDrawTransformer(transform)) {
// Drawn child image
// Set the input for the drawn image on the form
const drawImageIndex = appendDrawImage(
await streamToBlob(transform.child.stream)
);

// Tell the backend to run any transforms (possibly involving more draws)
// required to build this child
await walkTransforms(drawImageIndex, transform.child.transforms);

// Draw the child image on to the canvas
transforms.push({
drawImageIndex: drawImageIndex,
targetImageIndex: targetImageIndex,
...transform.options,
});
}
}
}

await walkTransforms(0, this.transforms);
body.append('transforms', JSON.stringify(transforms));
}
}

// Allow any as these are type guards
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isTransformer(input: any): input is ImageTransformerImpl {
return input instanceof ImageTransformerImpl;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isDrawTransformer(input: any): input is DrawTransformer {
return input instanceof DrawTransformer;
}

class ImagesBindingImpl implements ImagesBinding {
Expand Down
23 changes: 22 additions & 1 deletion src/cloudflare/internal/images.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@ type ImageTransform = {
zoom?: number;
};

type ImageDrawOptions = {
opacity?: number;
repeat?: boolean | string;
top?: number;
left?: number;
bottom?: number;
right?: number;
};

type ImageOutputOptions = {
format:
| 'image/jpeg'
Expand Down Expand Up @@ -93,10 +102,22 @@ interface ImagesBinding {
interface ImageTransformer {
/**
* Apply transform next, returning a transform handle.
* You can then apply more transformations or retrieve the output.
* You can then apply more transformations, draw, or retrieve the output.
* @param transform
*/
transform(transform: ImageTransform): ImageTransformer;

/**
* Draw an image on this transformer, returning a transform handle.
* You can then apply more transformations, draw, or retrieve the output.
* @param image The image (or transformer that will give the image) to draw
* @param options The options configuring how to draw the image
*/
draw(
image: ReadableStream<Uint8Array> | ImageTransformer,
options?: ImageDrawOptions
): ImageTransformer;

/**
* Retrieve the image that results from applying the transforms to the
* provided input
Expand Down
Loading

0 comments on commit 5b8cd6e

Please sign in to comment.