Summary
env.IMAGES.input(arrayBuffer).transform(...).output(...) throws in production:
TypeError: Cannot read properties of undefined (reading 'font')
JPEG/PNG from fetch().arrayBuffer(), R2Object.arrayBuffer(), and File.arrayBuffer() all fail. new Blob([buffer]).stream() works.
This is not a bad image. The text() type guard treats every non-ReadableStream as a TextRasterize.
Where
src/cloudflare/internal/images-api.ts @ 6552d999
Guard (too wide):
|
function isTextSource(source: ImageSource): source is TextRasterize { |
|
return !(source instanceof ReadableStream); |
|
} |
function isTextSource(source: ImageSource): source is TextRasterize {
return !(source instanceof ReadableStream);
}
Crash:
|
function serializeTextSource(source: TextRasterize): string { |
|
const obj: Record<string, string | number | { url: string }> = { |
|
text: source.content, |
|
font: source.options.font, |
|
}; |
function serializeTextSource(source: TextRasterize): string {
const obj = {
text: source.content,
font: source.options.font, // ArrayBuffer → options === undefined
};
Called from output():
|
if (isTextSource(this.#source)) { |
|
span.setAttribute('cloudflare.images.canvas.type', 'text'); |
|
formData.append('text_input', serializeTextSource(this.#source)); |
text() is the only correct constructor for that shape:
|
text(content: string, options: TextOptions): ImageTransformer { |
|
return new ImageTransformerImpl(this.#fetcher, { content, options }); |
|
} |
Types already say ImageSource = ReadableStream | TextRasterize and input(stream: ReadableStream):
|
type ImageSource = ReadableStream<Uint8Array> | TextRasterize; |
|
input( |
|
stream: ReadableStream<Uint8Array>, |
|
options?: ImageInputOptions |
|
): ImageTransformer; |
|
/** |
|
* Begin applying a series of transformations to text |
|
* @param content string to be rendered |
|
* @param options font, optional color and size to use in rendering text |
|
* @returns A transform handle |
|
*/ |
|
text(content: string, options: TextOptions): ImageTransformer; |
JS still accepts an ArrayBuffer at runtime. Same guard is used for draw overlays (#L219).
Why this is a regression
Docs still tell people to pass bytes:
hosted.upload() still accepts ArrayBuffer (images.d.ts#L270). input() used to as well.
Repro
export default {
async fetch(request, env) {
const origin = await fetch("https://cf-assets.www.cloudflare.com/dzlvafdwdttg/6Lx8xYhQ3h4bS3kYxYzYzY/x/cloudflare.png");
const buf = await origin.arrayBuffer();
return (
await env.IMAGES.input(buf).transform({ width: 200 }).output({ format: "image/webp" })
).response();
},
};
Expected: WebP.
Actual: TypeError: Cannot read properties of undefined (reading 'font').
env.IMAGES.input(new Blob([buf]).stream()) // works
Suggested fix
Detect text by shape, not “not a stream”:
function isTextSource(source: ImageSource): source is TextRasterize {
return (
typeof source === "object" &&
source !== null &&
"content" in source &&
"options" in source &&
typeof source.content === "string"
);
}
And/or wrap bytes in input() so the blog + R2/fetch().arrayBuffer() keep working:
input(image, options) {
const stream =
image instanceof ReadableStream ? image : new Blob([image]).stream();
// existing decode / ImageTransformerImpl
}
If you reject bytes, throw IMAGES_TRANSFORM_ERROR (“input() expects a ReadableStream”) instead of reading .font.
Workaround
env.IMAGES.input(new Blob([buffer], { type }).stream()).
Summary
env.IMAGES.input(arrayBuffer).transform(...).output(...)throws in production:JPEG/PNG from
fetch().arrayBuffer(),R2Object.arrayBuffer(), andFile.arrayBuffer()all fail.new Blob([buffer]).stream()works.This is not a bad image. The
text()type guard treats every non-ReadableStreamas aTextRasterize.Where
src/cloudflare/internal/images-api.ts@6552d999Guard (too wide):
workerd/src/cloudflare/internal/images-api.ts
Lines 16 to 18 in 6552d99
Crash:
workerd/src/cloudflare/internal/images-api.ts
Lines 20 to 24 in 6552d99
Called from
output():workerd/src/cloudflare/internal/images-api.ts
Lines 151 to 153 in 6552d99
text()is the only correct constructor for that shape:workerd/src/cloudflare/internal/images-api.ts
Lines 417 to 419 in 6552d99
Types already say
ImageSource = ReadableStream | TextRasterizeandinput(stream: ReadableStream):workerd/types/defines/images.d.ts
Line 36 in 6552d99
workerd/types/defines/images.d.ts
Lines 310 to 320 in 6552d99
JS still accepts an
ArrayBufferat runtime. Same guard is used for draw overlays (#L219).Why this is a regression
Docs still tell people to pass bytes:
const bytes = await env.IMAGES.hosted.image("IMAGE_ID").bytes();thenenv.IMAGES.input(bytes)const fileBuffer = await file.arrayBuffer();thenenv.IMAGES.input(fileBuffer)hosted.upload()still acceptsArrayBuffer(images.d.ts#L270).input()used to as well.Repro
Expected: WebP.
Actual:
TypeError: Cannot read properties of undefined (reading 'font').Suggested fix
Detect text by shape, not “not a stream”:
And/or wrap bytes in
input()so the blog + R2/fetch().arrayBuffer()keep working:If you reject bytes, throw
IMAGES_TRANSFORM_ERROR(“input()expects a ReadableStream”) instead of reading.font.Workaround
env.IMAGES.input(new Blob([buffer], { type }).stream()).