Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IMAGES-1355: Implement draw functionality in Images binding #3026

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
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
Loading