-
Notifications
You must be signed in to change notification settings - Fork 758
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
476 additions
and
14 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
import { File } from "buffer"; | ||
import sharp from "sharp"; | ||
import { z } from "zod"; | ||
import type { ImageInfoResponse } from "@cloudflare/workers-types/experimental"; | ||
import type { Sharp } from "sharp"; | ||
|
||
const Transform = z.object({ | ||
imageIndex: z.number().optional(), | ||
rotate: z.number().optional(), | ||
width: z.number().optional(), | ||
height: z.number().optional(), | ||
}); | ||
|
||
const Transforms = z.array(Transform); | ||
|
||
export async function imagesLocalFetcher(request: Request): Promise<Response> { | ||
const data = await request.formData(); | ||
|
||
const body = data.get("image"); | ||
if (!body || !(body instanceof File)) { | ||
return errorResponse(400, 9523, "ERROR: Expected image in request"); | ||
} | ||
|
||
const transformer = sharp(await body.arrayBuffer(), {}); | ||
|
||
const url = new URL(request.url); | ||
|
||
if (url.pathname == "/info") { | ||
return runInfo(transformer); | ||
} else { | ||
const badTransformsResponse = errorResponse( | ||
400, | ||
9523, | ||
"ERROR: Expected JSON array of valid transforms in transforms field" | ||
); | ||
try { | ||
const transformsJson = data.get("transforms"); | ||
|
||
if (typeof transformsJson !== "string") { | ||
return badTransformsResponse; | ||
} | ||
|
||
const transforms = Transforms.safeParse(JSON.parse(transformsJson)); | ||
|
||
if (!transforms.success) { | ||
return badTransformsResponse; | ||
} | ||
|
||
const outputFormat = data.get("output_format"); | ||
|
||
if (outputFormat != null && typeof outputFormat !== "string") { | ||
return errorResponse( | ||
400, | ||
9523, | ||
"ERROR: Expected output format to be a string if provided" | ||
); | ||
} | ||
|
||
return runTransform(transformer, transforms.data, outputFormat); | ||
} catch (e) { | ||
return badTransformsResponse; | ||
} | ||
} | ||
} | ||
|
||
async function runInfo(transformer: Sharp): Promise<Response> { | ||
const metadata = await transformer.metadata(); | ||
|
||
let mime: string | null = null; | ||
switch (metadata.format) { | ||
case "jpeg": | ||
mime = "image/jpeg"; | ||
break; | ||
case "svg": | ||
mime = "image/svg+xml"; | ||
break; | ||
case "png": | ||
mime = "image/png"; | ||
break; | ||
case "webp": | ||
mime = "image/webp"; | ||
break; | ||
case "gif": | ||
mime = "image/gif"; | ||
break; | ||
case "avif": | ||
mime = "image/avif"; | ||
break; | ||
default: | ||
return errorResponse(415, 9520, "ERROR: Unsupported image type"); | ||
} | ||
|
||
let resp: ImageInfoResponse; | ||
if (mime == "image/svg+xml") { | ||
resp = { | ||
format: mime, | ||
}; | ||
} else { | ||
if (!metadata.size || !metadata.width || !metadata.height) { | ||
return errorResponse( | ||
500, | ||
9523, | ||
"ERROR: Expected size, width and height for bitmap input" | ||
); | ||
} | ||
|
||
resp = { | ||
format: mime, | ||
fileSize: metadata.size, | ||
width: metadata.width, | ||
height: metadata.height, | ||
}; | ||
} | ||
|
||
return Response.json(resp); | ||
} | ||
|
||
async function runTransform( | ||
transformer: Sharp, | ||
transforms: z.infer<typeof Transforms>, | ||
outputFormat: string | null | ||
): Promise<Response> { | ||
for (const transform of transforms) { | ||
if (transform.imageIndex !== undefined && transform.imageIndex !== 0) { | ||
// We don't support draws, and this transform doesn't apply to the root | ||
// image, so skip it | ||
continue; | ||
} | ||
|
||
if (transform.rotate !== undefined) { | ||
transformer.rotate(transform.rotate); | ||
} | ||
|
||
if (transform.width !== undefined || transform.height !== undefined) { | ||
transformer.resize(transform.width || null, transform.height || null, { | ||
fit: "contain", | ||
}); | ||
} | ||
} | ||
|
||
switch (outputFormat) { | ||
case "image/avif": | ||
transformer.avif(); | ||
break; | ||
case "image/gif": | ||
return errorResponse( | ||
415, | ||
9520, | ||
"ERROR: GIF output is not supported in local mode" | ||
); | ||
case "image/jpeg": | ||
transformer.jpeg(); | ||
break; | ||
case "image/png": | ||
transformer.png(); | ||
break; | ||
case "image/webp": | ||
transformer.webp(); | ||
break; | ||
case "rgb": | ||
case "rgba": | ||
return errorResponse( | ||
415, | ||
9520, | ||
"ERROR: RGB/RGBA output is not supported in local mode" | ||
); | ||
default: | ||
outputFormat = "image/jpeg"; | ||
break; | ||
} | ||
|
||
return new Response(transformer, { | ||
headers: { | ||
"content-type": outputFormat, | ||
}, | ||
}); | ||
} | ||
|
||
function errorResponse(status: number, code: number, message: string) { | ||
return new Response(`ERROR ${code}: ${message}`, { | ||
status, | ||
headers: { | ||
"content-type": "text/plain", | ||
"cf-images-binding": `err=${code}`, | ||
}, | ||
}); | ||
} |
Oops, something went wrong.