691 lines
23 KiB
JavaScript
691 lines
23 KiB
JavaScript
import { error, warn } from "@opennextjs/aws/adapters/logger.js";
|
|
/**
|
|
* Handles requests to /_next/image(/), including image optimizations.
|
|
*
|
|
* Image optimization is disabled and the original image is returned if `env.IMAGES` is undefined.
|
|
*
|
|
* Throws an exception on unexpected errors.
|
|
*
|
|
* @param requestURL
|
|
* @param requestHeaders
|
|
* @param env
|
|
* @returns A promise that resolves to the resolved request.
|
|
*/
|
|
export async function handleImageRequest(requestURL, requestHeaders, env) {
|
|
const parseResult = parseImageRequest(requestURL, requestHeaders);
|
|
if (!parseResult.ok) {
|
|
return new Response(parseResult.message, {
|
|
status: 400,
|
|
});
|
|
}
|
|
let imageResponse;
|
|
if (parseResult.url.startsWith("/")) {
|
|
if (env.ASSETS === undefined) {
|
|
error("env.ASSETS binding is not defined");
|
|
return new Response('"url" parameter is valid but upstream response is invalid', {
|
|
status: 404,
|
|
});
|
|
}
|
|
const absoluteURL = new URL(parseResult.url, requestURL);
|
|
imageResponse = await env.ASSETS.fetch(absoluteURL);
|
|
}
|
|
else {
|
|
let fetchImageResult;
|
|
try {
|
|
fetchImageResult = await fetchWithRedirects(parseResult.url, 7_000, __IMAGES_MAX_REDIRECTS__);
|
|
}
|
|
catch (e) {
|
|
throw new Error("Failed to fetch image", { cause: e });
|
|
}
|
|
if (!fetchImageResult.ok) {
|
|
if (fetchImageResult.error === "timed_out") {
|
|
return new Response('"url" parameter is valid but upstream response timed out', {
|
|
status: 504,
|
|
});
|
|
}
|
|
if (fetchImageResult.error === "too_many_redirects") {
|
|
return new Response('"url" parameter is valid but upstream response is invalid', {
|
|
status: 508,
|
|
});
|
|
}
|
|
throw new Error("Failed to fetch image");
|
|
}
|
|
imageResponse = fetchImageResult.response;
|
|
}
|
|
if (!imageResponse.ok || imageResponse.body === null) {
|
|
return new Response('"url" parameter is valid but upstream response is invalid', {
|
|
status: imageResponse.status,
|
|
});
|
|
}
|
|
let immutable = false;
|
|
if (parseResult.static) {
|
|
immutable = true;
|
|
}
|
|
else {
|
|
const cacheControlHeader = imageResponse.headers.get("Cache-Control");
|
|
if (cacheControlHeader !== null) {
|
|
// TODO: Properly parse header
|
|
immutable = cacheControlHeader.includes("immutable");
|
|
}
|
|
}
|
|
const readHeaderResult = await readImageHeader(imageResponse);
|
|
if (readHeaderResult instanceof Response) {
|
|
return readHeaderResult;
|
|
}
|
|
const { contentType, imageStream } = readHeaderResult;
|
|
if (contentType === null) {
|
|
warn(`Failed to detect content type of "${parseResult.url}"`);
|
|
return new Response('"url" parameter is valid but image type is not allowed', {
|
|
status: 400,
|
|
});
|
|
}
|
|
if (contentType === SVG) {
|
|
if (!__IMAGES_ALLOW_SVG__) {
|
|
return new Response('"url" parameter is valid but image type is not allowed', {
|
|
status: 400,
|
|
});
|
|
}
|
|
const response = createImageResponse(imageStream, contentType, {
|
|
immutable,
|
|
});
|
|
return response;
|
|
}
|
|
if (contentType === GIF) {
|
|
if (env.IMAGES === undefined) {
|
|
warn("env.IMAGES binding is not defined");
|
|
const response = createImageResponse(imageStream, contentType, {
|
|
immutable,
|
|
});
|
|
return response;
|
|
}
|
|
const imageSource = env.IMAGES.input(imageStream);
|
|
const imageTransformationResult = await imageSource
|
|
.transform({
|
|
width: parseResult.width,
|
|
fit: "scale-down",
|
|
})
|
|
.output({
|
|
quality: parseResult.quality,
|
|
format: GIF,
|
|
});
|
|
const outputImageStream = imageTransformationResult.image();
|
|
const response = createImageResponse(outputImageStream, GIF, {
|
|
immutable,
|
|
});
|
|
return response;
|
|
}
|
|
if (contentType === AVIF || contentType === WEBP || contentType === JPEG || contentType === PNG) {
|
|
if (env.IMAGES === undefined) {
|
|
warn("env.IMAGES binding is not defined");
|
|
const response = createImageResponse(imageStream, contentType, {
|
|
immutable,
|
|
});
|
|
return response;
|
|
}
|
|
const outputFormat = parseResult.format ?? contentType;
|
|
const imageSource = env.IMAGES.input(imageStream);
|
|
const imageTransformationResult = await imageSource
|
|
.transform({
|
|
width: parseResult.width,
|
|
fit: "scale-down",
|
|
})
|
|
.output({
|
|
quality: parseResult.quality,
|
|
format: outputFormat,
|
|
});
|
|
const outputImageStream = imageTransformationResult.image();
|
|
const response = createImageResponse(outputImageStream, outputFormat, {
|
|
immutable,
|
|
});
|
|
return response;
|
|
}
|
|
warn(`Image content type ${contentType} not supported`);
|
|
const response = createImageResponse(imageStream, contentType, {
|
|
immutable,
|
|
});
|
|
return response;
|
|
}
|
|
/**
|
|
* Handles requests to /cdn-cgi/image/ in development.
|
|
*
|
|
* Extracts the image URL, fetches the image, and checks the content type against
|
|
* Cloudflare's supported input formats.
|
|
*
|
|
* @param requestURL The full request URL.
|
|
* @param env The Cloudflare environment bindings.
|
|
* @returns A promise that resolves to the image response.
|
|
*/
|
|
export async function handleCdnCgiImageRequest(requestURL, env) {
|
|
const parseResult = parseCdnCgiImageRequest(requestURL.pathname);
|
|
if (!parseResult.ok) {
|
|
return new Response(parseResult.message, {
|
|
status: 400,
|
|
});
|
|
}
|
|
let imageResponse;
|
|
if (parseResult.url.startsWith("/")) {
|
|
if (env.ASSETS === undefined) {
|
|
return new Response("env.ASSETS binding is not defined", {
|
|
status: 404,
|
|
});
|
|
}
|
|
const absoluteURL = new URL(parseResult.url, requestURL);
|
|
imageResponse = await env.ASSETS.fetch(absoluteURL);
|
|
}
|
|
else {
|
|
imageResponse = await fetch(parseResult.url);
|
|
}
|
|
if (!imageResponse.ok || imageResponse.body === null) {
|
|
return new Response('"url" parameter is valid but upstream response is invalid', {
|
|
status: imageResponse.status,
|
|
});
|
|
}
|
|
const readHeaderResult = await readImageHeader(imageResponse);
|
|
if (readHeaderResult instanceof Response) {
|
|
return readHeaderResult;
|
|
}
|
|
const { contentType, imageStream } = readHeaderResult;
|
|
if (contentType === null || !SUPPORTED_CDN_CGI_INPUT_TYPES.has(contentType)) {
|
|
return new Response('"url" parameter is valid but image type is not allowed', {
|
|
status: 400,
|
|
});
|
|
}
|
|
if (contentType === SVG && !__IMAGES_ALLOW_SVG__) {
|
|
return new Response('"url" parameter is valid but image type is not allowed', {
|
|
status: 400,
|
|
});
|
|
}
|
|
return new Response(imageStream, {
|
|
headers: { "Content-Type": contentType },
|
|
});
|
|
}
|
|
/**
|
|
* Parses a /cdn-cgi/image/ request URL.
|
|
*
|
|
* Extracts the image URL from the `/cdn-cgi/image/<options>/<image-url>` path format.
|
|
* Rejects protocol-relative URLs (`//...`). The cdn-cgi options are not parsed or
|
|
* validated as they are Cloudflare's concern.
|
|
*
|
|
* @param pathname The URL pathname (e.g. `/cdn-cgi/image/width=640,quality=75,format=auto/path/to/image.png`).
|
|
* @returns the parsed URL result or an error.
|
|
*/
|
|
export function parseCdnCgiImageRequest(pathname) {
|
|
const match = pathname.match(/^\/cdn-cgi\/image\/(?<options>[^/]+)\/(?<url>.+)$/);
|
|
if (match === null ||
|
|
// Valid URLs have at least one option
|
|
!match.groups?.options ||
|
|
!match.groups?.url) {
|
|
return { ok: false, message: "Invalid /cdn-cgi/image/ URL format" };
|
|
}
|
|
const imageUrl = match.groups.url;
|
|
// The regex separator consumes one `/`, so if imageUrl starts with `/`
|
|
// the original URL segment was protocol-relative (`//...`).
|
|
if (imageUrl.startsWith("/")) {
|
|
return { ok: false, message: '"url" parameter cannot be a protocol-relative URL (//)' };
|
|
}
|
|
// Resolve the image URL: it may be absolute (https://...) or relative.
|
|
let resolvedUrl;
|
|
if (imageUrl.match(/^https?:\/\//)) {
|
|
resolvedUrl = imageUrl;
|
|
}
|
|
else {
|
|
// Relative URLs need a leading slash.
|
|
resolvedUrl = `/${imageUrl}`;
|
|
}
|
|
return {
|
|
ok: true,
|
|
url: resolvedUrl,
|
|
static: false,
|
|
};
|
|
}
|
|
/**
|
|
* Reads the first 32 bytes of an image response to detect its content type.
|
|
*
|
|
* Tees the response body so the image stream can still be consumed after detection.
|
|
*
|
|
* @param imageResponse The image response whose body to read.
|
|
* @returns The detected content type and image stream, or an error Response if the header bytes
|
|
* could not be read.
|
|
*/
|
|
async function readImageHeader(imageResponse) {
|
|
// Note: imageResponse.body is non-null — callers check before calling.
|
|
const [contentTypeStream, imageStream] = imageResponse.body.tee();
|
|
const headerBytes = new Uint8Array(32);
|
|
const reader = contentTypeStream.getReader({ mode: "byob" });
|
|
const readResult = await reader.readAtLeast(32, headerBytes);
|
|
if (readResult.value === undefined) {
|
|
await imageResponse.body.cancel();
|
|
return new Response('"url" parameter is valid but upstream response is invalid', {
|
|
status: 400,
|
|
});
|
|
}
|
|
const contentType = detectImageContentType(readResult.value);
|
|
return { contentType, imageStream };
|
|
}
|
|
/**
|
|
* Fetch call with max redirects and timeouts.
|
|
*
|
|
* Re-throws the exception thrown by a fetch call.
|
|
* @param url
|
|
* @param timeoutMS Timeout for a single fetch call.
|
|
* @param maxRedirectCount
|
|
* @returns
|
|
*/
|
|
async function fetchWithRedirects(url, timeoutMS, maxRedirectCount) {
|
|
// TODO: Add dangerouslyAllowLocalIP support
|
|
let response;
|
|
try {
|
|
response = await fetch(url, {
|
|
signal: AbortSignal.timeout(timeoutMS),
|
|
redirect: "manual",
|
|
});
|
|
}
|
|
catch (e) {
|
|
if (e instanceof Error && e.name === "TimeoutError") {
|
|
const result = {
|
|
ok: false,
|
|
error: "timed_out",
|
|
};
|
|
return result;
|
|
}
|
|
throw e;
|
|
}
|
|
if (redirectResponseStatuses.includes(response.status)) {
|
|
const locationHeader = response.headers.get("Location");
|
|
if (locationHeader !== null) {
|
|
if (maxRedirectCount < 1) {
|
|
const result = {
|
|
ok: false,
|
|
error: "too_many_redirects",
|
|
};
|
|
return result;
|
|
}
|
|
let redirectTarget;
|
|
if (locationHeader.startsWith("/")) {
|
|
redirectTarget = new URL(locationHeader, url).href;
|
|
}
|
|
else {
|
|
redirectTarget = locationHeader;
|
|
}
|
|
const result = await fetchWithRedirects(redirectTarget, timeoutMS, maxRedirectCount - 1);
|
|
return result;
|
|
}
|
|
}
|
|
const result = {
|
|
ok: true,
|
|
response: response,
|
|
};
|
|
return result;
|
|
}
|
|
const redirectResponseStatuses = [301, 302, 303, 307, 308];
|
|
function createImageResponse(image, contentType, imageResponseFlags) {
|
|
const response = new Response(image, {
|
|
headers: {
|
|
Vary: "Accept",
|
|
"Content-Type": contentType,
|
|
"Content-Disposition": __IMAGES_CONTENT_DISPOSITION__,
|
|
"Content-Security-Policy": __IMAGES_CONTENT_SECURITY_POLICY__,
|
|
},
|
|
});
|
|
if (imageResponseFlags.immutable) {
|
|
response.headers.set("Cache-Control", "public, max-age=315360000, immutable");
|
|
}
|
|
return response;
|
|
}
|
|
/**
|
|
* Parses the image request URL and headers.
|
|
*
|
|
* This function validates the parameters and returns either the parsed result or an error message.
|
|
*
|
|
* @param requestURL request URL
|
|
* @param requestHeaders request headers
|
|
* @returns an instance of `ParseImageRequestURLSuccessResult` when successful, or an instance of `ErrorResult` when failed.
|
|
*/
|
|
function parseImageRequest(requestURL, requestHeaders) {
|
|
const formats = __IMAGES_FORMATS__;
|
|
const parsedUrlOrError = validateUrlQueryParameter(requestURL);
|
|
if (!("url" in parsedUrlOrError)) {
|
|
return parsedUrlOrError;
|
|
}
|
|
const widthOrError = validateWidthQueryParameter(requestURL);
|
|
if (typeof widthOrError !== "number") {
|
|
return widthOrError;
|
|
}
|
|
const qualityOrError = validateQualityQueryParameter(requestURL);
|
|
if (typeof qualityOrError !== "number") {
|
|
return qualityOrError;
|
|
}
|
|
const acceptHeader = requestHeaders.get("Accept") ?? "";
|
|
let format = null;
|
|
// Find a more specific format that the client accepts.
|
|
for (const allowedFormat of formats) {
|
|
if (acceptHeader.includes(allowedFormat)) {
|
|
format = allowedFormat;
|
|
break;
|
|
}
|
|
}
|
|
const result = {
|
|
ok: true,
|
|
url: parsedUrlOrError.url,
|
|
width: widthOrError,
|
|
quality: qualityOrError,
|
|
format,
|
|
static: parsedUrlOrError.static,
|
|
};
|
|
return result;
|
|
}
|
|
/**
|
|
* Validates that there is exactly one "url" query parameter.
|
|
*
|
|
* Checks length, protocol-relative URLs, local/remote pattern matching, recursion, and protocol.
|
|
*
|
|
* @param requestURL The request URL containing the "url" query parameter.
|
|
* @returns the validated URL or an error result.
|
|
*/
|
|
function validateUrlQueryParameter(requestURL) {
|
|
// There should be a single "url" parameter.
|
|
const urls = requestURL.searchParams.getAll("url");
|
|
if (urls.length < 1) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter is required',
|
|
};
|
|
return result;
|
|
}
|
|
if (urls.length > 1) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter cannot be an array',
|
|
};
|
|
return result;
|
|
}
|
|
const url = urls[0];
|
|
if (url.length > 3072) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter is too long',
|
|
};
|
|
return result;
|
|
}
|
|
if (url.startsWith("//")) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter cannot be a protocol-relative URL (//)',
|
|
};
|
|
return result;
|
|
}
|
|
if (url.startsWith("/")) {
|
|
const staticAsset = url.startsWith(`${__NEXT_BASE_PATH__ || ""}/_next/static/media`);
|
|
const pathname = getPathnameFromRelativeURL(url);
|
|
if (/\/_next\/image($|\/)/.test(decodeURIComponent(pathname))) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter cannot be recursive',
|
|
};
|
|
return result;
|
|
}
|
|
if (!staticAsset) {
|
|
if (!hasLocalMatch(__IMAGES_LOCAL_PATTERNS__, url)) {
|
|
const result = { ok: false, message: '"url" parameter is not allowed' };
|
|
return result;
|
|
}
|
|
}
|
|
return { url, static: staticAsset };
|
|
}
|
|
let parsedURL;
|
|
try {
|
|
parsedURL = new URL(url);
|
|
}
|
|
catch {
|
|
const result = { ok: false, message: '"url" parameter is invalid' };
|
|
return result;
|
|
}
|
|
const validProtocols = ["http:", "https:"];
|
|
if (!validProtocols.includes(parsedURL.protocol)) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter is invalid',
|
|
};
|
|
return result;
|
|
}
|
|
if (!hasRemoteMatch(__IMAGES_REMOTE_PATTERNS__, parsedURL)) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"url" parameter is not allowed',
|
|
};
|
|
return result;
|
|
}
|
|
return { url: parsedURL.href, static: false };
|
|
}
|
|
/**
|
|
* Validates the "w" (width) query parameter.
|
|
*
|
|
* @returns the validated width number or an error result.
|
|
*/
|
|
function validateWidthQueryParameter(requestURL) {
|
|
const widthQueryValues = requestURL.searchParams.getAll("w");
|
|
if (widthQueryValues.length < 1) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"w" parameter (width) is required',
|
|
};
|
|
return result;
|
|
}
|
|
if (widthQueryValues.length > 1) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"w" parameter (width) cannot be an array',
|
|
};
|
|
return result;
|
|
}
|
|
const widthQueryValue = widthQueryValues[0];
|
|
if (!/^[0-9]+$/.test(widthQueryValue)) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"w" parameter (width) must be an integer greater than 0',
|
|
};
|
|
return result;
|
|
}
|
|
const width = parseInt(widthQueryValue, 10);
|
|
if (width <= 0 || isNaN(width)) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"w" parameter (width) must be an integer greater than 0',
|
|
};
|
|
return result;
|
|
}
|
|
const sizeValid = __IMAGES_DEVICE_SIZES__.includes(width) || __IMAGES_IMAGE_SIZES__.includes(width);
|
|
if (!sizeValid) {
|
|
const result = {
|
|
ok: false,
|
|
message: `"w" parameter (width) of ${width} is not allowed`,
|
|
};
|
|
return result;
|
|
}
|
|
return width;
|
|
}
|
|
/**
|
|
* Validates the "q" (quality) query parameter.
|
|
*
|
|
* @returns the validated quality number or an error result.
|
|
*/
|
|
function validateQualityQueryParameter(requestURL) {
|
|
const qualityQueryValues = requestURL.searchParams.getAll("q");
|
|
if (qualityQueryValues.length < 1) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"q" parameter (quality) is required',
|
|
};
|
|
return result;
|
|
}
|
|
if (qualityQueryValues.length > 1) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"q" parameter (quality) cannot be an array',
|
|
};
|
|
return result;
|
|
}
|
|
const qualityQueryValue = qualityQueryValues[0];
|
|
if (!/^[0-9]+$/.test(qualityQueryValue)) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"q" parameter (quality) must be an integer between 1 and 100',
|
|
};
|
|
return result;
|
|
}
|
|
const quality = parseInt(qualityQueryValue, 10);
|
|
if (isNaN(quality) || quality < 1 || quality > 100) {
|
|
const result = {
|
|
ok: false,
|
|
message: '"q" parameter (quality) must be an integer between 1 and 100',
|
|
};
|
|
return result;
|
|
}
|
|
if (!__IMAGES_QUALITIES__.includes(quality)) {
|
|
const result = {
|
|
ok: false,
|
|
message: `"q" parameter (quality) of ${quality} is not allowed`,
|
|
};
|
|
return result;
|
|
}
|
|
return quality;
|
|
}
|
|
function getPathnameFromRelativeURL(relativeURL) {
|
|
return relativeURL.split("?")[0];
|
|
}
|
|
function hasLocalMatch(localPatterns, relativeURL) {
|
|
const parseRelativeURLResult = parseRelativeURL(relativeURL);
|
|
for (const localPattern of localPatterns) {
|
|
const matched = matchLocalPattern(localPattern, parseRelativeURLResult);
|
|
if (matched) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
function parseRelativeURL(relativeURL) {
|
|
if (!relativeURL.includes("?")) {
|
|
const result = {
|
|
pathname: relativeURL,
|
|
search: "",
|
|
};
|
|
return result;
|
|
}
|
|
const parts = relativeURL.split("?");
|
|
const pathname = parts[0];
|
|
const search = "?" + parts.slice(1).join("?");
|
|
const result = {
|
|
pathname,
|
|
search,
|
|
};
|
|
return result;
|
|
}
|
|
export function matchLocalPattern(pattern, url) {
|
|
if (pattern.search !== undefined && pattern.search !== url.search) {
|
|
return false;
|
|
}
|
|
return new RegExp(pattern.pathname).test(url.pathname);
|
|
}
|
|
function hasRemoteMatch(remotePatterns, url) {
|
|
for (const remotePattern of remotePatterns) {
|
|
const matched = matchRemotePattern(remotePattern, url);
|
|
if (matched) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
export function matchRemotePattern(pattern, url) {
|
|
// https://github.com/vercel/next.js/blob/d76f0b1/packages/next/src/shared/lib/match-remote-pattern.ts
|
|
if (pattern.protocol !== undefined &&
|
|
pattern.protocol.replace(/:$/, "") !== url.protocol.replace(/:$/, "")) {
|
|
return false;
|
|
}
|
|
if (pattern.port !== undefined && pattern.port !== url.port) {
|
|
return false;
|
|
}
|
|
if (pattern.hostname === undefined || !new RegExp(pattern.hostname).test(url.hostname)) {
|
|
return false;
|
|
}
|
|
if (pattern.search !== undefined && pattern.search !== url.search) {
|
|
return false;
|
|
}
|
|
// Should be the same as writeImagesManifest()
|
|
return new RegExp(pattern.pathname).test(url.pathname);
|
|
}
|
|
const AVIF = "image/avif";
|
|
const WEBP = "image/webp";
|
|
const PNG = "image/png";
|
|
const JPEG = "image/jpeg";
|
|
const JXL = "image/jxl";
|
|
const JP2 = "image/jp2";
|
|
const HEIC = "image/heic";
|
|
const GIF = "image/gif";
|
|
const SVG = "image/svg+xml";
|
|
const ICO = "image/x-icon";
|
|
const ICNS = "image/x-icns";
|
|
const TIFF = "image/tiff";
|
|
const BMP = "image/bmp";
|
|
/**
|
|
* Image content types supported as input by Cloudflare's cdn-cgi image transformation.
|
|
*
|
|
* @see https://developers.cloudflare.com/images/transform-images/#supported-input-formats
|
|
*/
|
|
const SUPPORTED_CDN_CGI_INPUT_TYPES = new Set([JPEG, PNG, GIF, WEBP, SVG, HEIC]);
|
|
/**
|
|
* Detects the content type by looking at the first few bytes of a file
|
|
*
|
|
* Based on https://github.com/vercel/next.js/blob/72c9635/packages/next/src/server/image-optimizer.ts#L155
|
|
*
|
|
* @param buffer The image bytes
|
|
* @returns a content type of undefined for unsupported content
|
|
*/
|
|
export function detectImageContentType(buffer) {
|
|
if ([0xff, 0xd8, 0xff].every((b, i) => buffer[i] === b)) {
|
|
return JPEG;
|
|
}
|
|
if ([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a].every((b, i) => buffer[i] === b)) {
|
|
return PNG;
|
|
}
|
|
if ([0x47, 0x49, 0x46, 0x38].every((b, i) => buffer[i] === b)) {
|
|
return GIF;
|
|
}
|
|
if ([0x52, 0x49, 0x46, 0x46, 0, 0, 0, 0, 0x57, 0x45, 0x42, 0x50].every((b, i) => !b || buffer[i] === b)) {
|
|
return WEBP;
|
|
}
|
|
if ([0x3c, 0x3f, 0x78, 0x6d, 0x6c].every((b, i) => buffer[i] === b)) {
|
|
return SVG;
|
|
}
|
|
if ([0x3c, 0x73, 0x76, 0x67].every((b, i) => buffer[i] === b)) {
|
|
return SVG;
|
|
}
|
|
if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x61, 0x76, 0x69, 0x66].every((b, i) => !b || buffer[i] === b)) {
|
|
return AVIF;
|
|
}
|
|
if ([0x00, 0x00, 0x01, 0x00].every((b, i) => buffer[i] === b)) {
|
|
return ICO;
|
|
}
|
|
if ([0x69, 0x63, 0x6e, 0x73].every((b, i) => buffer[i] === b)) {
|
|
return ICNS;
|
|
}
|
|
if ([0x49, 0x49, 0x2a, 0x00].every((b, i) => buffer[i] === b)) {
|
|
return TIFF;
|
|
}
|
|
if ([0x42, 0x4d].every((b, i) => buffer[i] === b)) {
|
|
return BMP;
|
|
}
|
|
if ([0xff, 0x0a].every((b, i) => buffer[i] === b)) {
|
|
return JXL;
|
|
}
|
|
if ([0x00, 0x00, 0x00, 0x0c, 0x4a, 0x58, 0x4c, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)) {
|
|
return JXL;
|
|
}
|
|
if ([0, 0, 0, 0, 0x66, 0x74, 0x79, 0x70, 0x68, 0x65, 0x69, 0x63].every((b, i) => !b || buffer[i] === b)) {
|
|
return HEIC;
|
|
}
|
|
if ([0x00, 0x00, 0x00, 0x0c, 0x6a, 0x50, 0x20, 0x20, 0x0d, 0x0a, 0x87, 0x0a].every((b, i) => buffer[i] === b)) {
|
|
return JP2;
|
|
}
|
|
return null;
|
|
}
|