react-thumbor-image
Version:
Makes it easy to add Thumbor optimized images to your React app.
511 lines (453 loc) • 18.6 kB
text/typescript
import type {AbstractThumborFilterProp, Color, HtmlColor} from "./thumborFilters";
//import HmacSHA1 from "crypto-js/hmac-sha1";
//import Base64 from "crypto-js/enc-base64";
import jsSHA from "jssha";
export function srcToUrlLocal(src: string): URL {
return new URL(src);
}
export function encodeURLLocal(src: URL) : string {
return encodeURIComponent(src.toString());
}
export interface ThumborTrimProps {
trimMode: "top-left" | "bottom-right" | "auto";
}
export interface ThumborFlipProps {
flipMode: "vertical" | "horizontal" | "both";
}
export interface ThumborPointProps {
x: number;
y: number;
}
export interface ThumborCropProps {
topLeft: ThumborPointProps;
bottomRight: ThumborPointProps;
}
export interface ThumborProps {
url: URL;
key?: string;
trim?: ThumborTrimProps;
crop?: ThumborCropProps;
fit?: "fit-in" | "adaptive-fit-in" | "full-fit-in";
width?: number | "orig";
height?: number | "orig";
flip?: ThumborFlipProps;
horizontalAlignment?: "left" | "center" | "right";
verticalAlignment?: "top" | "middle" | "bottom";
smart?: boolean;
filters?: AbstractThumborFilterProp[];
}
function isHtmlColor(color: string): color is HtmlColor {
return ['maroon', 'darkred', 'brown', 'firebrick', 'crimson', 'red', 'tomato', 'coral', 'indianred', 'lightcoral', 'darksalmon', 'salmon', 'lightsalmon', 'orangered', 'darkorange', 'orange', 'gold', 'darkgoldenrod', 'goldenrod', 'palegoldenrod', 'darkkhaki', 'khaki', 'olive', 'yellow', 'yellowgreen', 'darkolivegreen', 'olivedrab', 'lawngreen', 'chartreuse', 'greenyellow', 'darkgreen', 'green', 'forestgreen', 'lime', 'limegreen', 'lightgreen', 'palegreen', 'darkseagreen', 'mediumspringgreen', 'springgreen', 'seagreen', 'mediumaquamarine', 'mediumseagreen', 'lightseagreen', 'darkslategray', 'teal', 'darkcyan', 'aqua', 'cyan', 'lightcyan', 'darkturquoise', 'turquoise', 'mediumturquoise', 'paleturquoise', 'aquamarine', 'powderblue', 'cadetblue', 'steelblue', 'cornflowerblue', 'deepskyblue', 'dodgerblue', 'lightblue', 'skyblue', 'lightskyblue', 'midnightblue', 'navy', 'darkblue', 'mediumblue', 'blue', 'royalblue', 'blueviolet', 'indigo', 'darkslateblue', 'slateblue', 'mediumslateblue', 'mediumpurple', 'darkmagenta', 'darkviolet', 'darkorchid', 'mediumorchid', 'purple', 'thistle', 'plum', 'violet', 'fuchsia', 'orchid', 'mediumvioletred', 'palevioletred', 'deeppink', 'hotpink', 'lightpink', 'pink', 'antiquewhite', 'beige', 'bisque', 'blanchedalmond', 'wheat', 'cornsilk', 'lemonchiffon', 'lightgoldenrodyellow', 'lightyellow', 'saddlebrown', 'sienna', 'chocolate', 'peru', 'sandybrown', 'burlywood', 'tan', 'rosybrown', 'moccasin', 'navajowhite', 'peachpuff', 'mistyrose', 'lavenderblush', 'linen', 'oldlace', 'papayawhip', 'seashell', 'mintcream', 'slategray', 'lightslategray', 'lightsteelblue', 'lavender', 'floralwhite', 'aliceblue', 'ghostwhite', 'honeydew', 'ivory', 'azure', 'snow', 'black', 'dimgray', 'gray', 'darkgray', 'silver', 'lightgray', 'gainsboro', 'whitesmoke', 'white'].indexOf(color) !== -1;
}
const isHexColourCode = (s: string) => !!s.match(/^#[a-f0-9]{3}([a-f0-9]{3})?$/i)
function isEncoded(str: string) {
return typeof str == "string" && decodeURIComponent(str) !== str;
}
type FilterPreprocessorResult = {
filterString: string,
extractionProps?: ThumborCropProps
};
function buildFilterString(filter: AbstractThumborFilterProp): FilterPreprocessorResult {
let filterString = '';
let extractionProps: ThumborCropProps | null = null;
switch (filter.name) {
case 'autojpg':
filterString += filter.name+'()';
break;
case 'background_color':
if(!isHexColourCode(filter.color) && filter.color !== 'auto' && !isHtmlColor(filter.color)) {
filter.color = 'auto';
}
else if(isHexColourCode(filter.color)) {
filter.color = filter.color.replace('#', '');
}
filterString += filter.name+'('+filter.color+')';
break;
case 'blur':
if(filter.radius < 0) {
filter.radius = 0;
}
else if(filter.radius > 150) {
filter.radius = 150;
}
if(filter.sigma < 0) {
filter.sigma = 0;
}
else if(filter.sigma > 150) {
filter.sigma = 150;
}
filterString += filter.name + '('+filter.radius+(filter.sigma ? ','+filter.sigma : '')+')';
break;
case 'brightness':
filterString += filter.name;
if(filter.amount < -100) {
filter.amount = -100;
}
else if(filter.amount > 100) {
filter.amount = 100;
}
filterString += '('+filter.amount+')';
break;
case 'contrast':
filterString += filter.name;
if(filter.amount < -100) {
filter.amount = -100;
}
else if(filter.amount > 100) {
filter.amount = 100;
}
filterString += '('+filter.amount+')';
break;
case 'convolution':
filterString += filter.name+='(';
filter.matrix.forEach((array) => {
array.forEach((value) => {
filterString += value + ';';
});
});
filterString = filterString.slice(0, -1);
filterString += ','+filter.columns;
filterString += ','+(filter.normalize ? 'true' : 'false')+')';
break;
case 'cover':
filterString += filter.name+"()";
break;
case 'equalize':
filterString += filter.name+"()";
break;
case 'extract_focal':
filterString += filter.name+'()';
extractionProps = filter.pre;
break;
case 'fill':
if(!isHexColourCode(filter.color) && filter.color !== 'auto' && !isHtmlColor(filter.color) && filter.color !== 'blur' && filter.color !== 'transparent') {
filter.color = 'auto';
}
else if(isHexColourCode(filter.color)) {
filter.color = filter.color.replace('#', '');
}
filterString += filter.name+'('+filter.color+(filter.fillTransparent ? ',true' : '')+')';
break;
case 'focal':
filterString += filter.name+`(${filter.left}x${filter.top}:${filter.right}x${filter.bottom})`;
break;
case 'format':
filterString += filter.name + `(${filter.format})`;
break;
case "grayscale":
filterString += filter.name+'()';
break;
case 'max_bytes':
filterString += filter.name+'('+filter.bytes+')';
break;
case "no_upscale":
filterString += filter.name+'()';
break;
case "noise":
if(filter.amount < 0) {
filter.amount = 0;
}
else if(filter.amount > 100) {
filter.amount = 100;
}
filterString += filter.name+`(${filter.amount})`;
break;
case "proportion":
if(filter.percentage < 0.0) {
filter.percentage = 0.0;
}
else if(filter.percentage > 1.0) {
filter.percentage = 1.0;
}
filterString += filter.name+`(${filter.percentage})`;
break;
case "quality":
if(filter.amount < 0) {
filter.amount = 0;
}
else if(filter.amount > 100) {
filter.amount = 100;
}
filterString += filter.name+`(${filter.amount})`;
break;
case "rgb":
if(filter.red < -100) {
filter.red = -100;
}
else if(filter.red > 100) {
filter.red = 100;
}
if(filter.green < -100) {
filter.green = -100;
}
else if(filter.green > 100) {
filter.green = 100;
}
if(filter.blue < -100) {
filter.blue = -100;
}
else if(filter.blue > 100) {
filter.blue = 100;
}
filterString += filter.name+`(${filter.red},${filter.green},${filter.blue})`;
break;
case "rotate":
if(filter.angle < 0) {
filter.angle = 0;
}
else if(filter.angle > 359) {
filter.angle = 359;
}
filterString += filter.name+`(${filter.angle})`;
break;
case "round_corner":
if(filter.red < 0) {
filter.red = 0;
}
else if(filter.red > 255) {
filter.red = 255;
}
if(filter.green < 0) {
filter.green = 0;
}
else if(filter.green > 255) {
filter.green = 255;
}
if(filter.blue < 0) {
filter.blue = 0;
}
else if(filter.blue > 255) {
filter.blue = 255;
}
filterString += filter.name+`(${filter.a}`;
if(filter.b) {
filterString += `|${filter.b}`;
}
filterString += `,${filter.red},${filter.green},${filter.blue}`;
if(filter.transparent) {
filterString += `,true`;
}
filterString += `)`;
break;
case "saturation":
filterString += filter.name+`(${filter.amount})`;
break;
case "sharpen":
filterString += filter.name+`(${filter.amount},${filter.radius},${filter.luminance? 'true': 'false'})`;
break;
case "stretch":
filterString += filter.name+'()';
break;
case "strip_exif":
filterString += "strip\_exif"+'()';
break;
case "strip_icc":
filterString += "strip\_icc"+'()';
break;
case "upscale":
filterString += filter.name+'()';
break;
case "watermark":
if(!isEncoded(filter.url)) {
filter.url = encodeURIComponent(filter.url);
}
filterString += filter.name+`(${filter.url},`;
switch (filter.xPosition.type) {
case 'number':
filterString += `${filter.xPosition.value},`;
break;
case 'percentage':
filterString += `${filter.xPosition.value}p,`;
break;
case 'center':
filterString += `center,`;
break;
case 'repeat':
filterString += `repeat,`;
break;
}
switch (filter.yPosition.type) {
case 'number':
filterString += `${filter.yPosition.value},`;
break;
case 'percentage':
filterString += `${filter.yPosition.value}p,`;
break;
case 'center':
filterString += `center,`;
break;
case 'repeat':
filterString += `repeat,`;
break;
}
if(filter.alpha < 0) {
filter.alpha = 0;
}
else if(filter.alpha > 100) {
filter.alpha = 100;
}
filterString += `${filter.alpha}${filter.wRatio? ','+filter.wRatio : ''}${filter.hRatio ? ','+filter.hRatio : ''})`;
break;
}
return extractionProps ? {
filterString,
extractionProps
} : {
filterString
};
}
function buildFiltersString(filters: AbstractThumborFilterProp[]) : FilterPreprocessorResult {
let filtersString = '/filters:';
let extractionProps: ThumborCropProps | null = null;
if(filters && filters.length > 0) {
filters.forEach((filter, index) => {
let result = buildFilterString(filter);
filtersString += result.filterString;
if(filter.name === 'extract_focal' && result.extractionProps) {
extractionProps = result.extractionProps;
}
if(index < filters.length - 1) {
filtersString += ':';
}
});
}
return extractionProps ? {
filterString: filtersString,
extractionProps
} : {
filterString: filtersString
};
}
export function buildUrl(imageSrc: string, thumborProps: ThumborProps): URL {
let url = `${thumborProps.url.toString()}`;
url = url.endsWith("/") ? url.substring(0, url.length - 1) : url;
let mainUrlPart = '';
if(thumborProps.trim) {
if(thumborProps.trim.trimMode === "auto") {
mainUrlPart += `/trim`;
}
else {
mainUrlPart += `/trim:${thumborProps.trim.trimMode}`;
}
}
if(thumborProps.crop) {
mainUrlPart += `/${thumborProps.crop.topLeft.x}x${thumborProps.crop.topLeft.y}:${thumborProps.crop.bottomRight.x}x${thumborProps.crop.bottomRight.y}`;
}
if(thumborProps.fit) {
if(thumborProps.fit === "fit-in") {
mainUrlPart += `/fit-in`;
}
else if(thumborProps.fit === "adaptive-fit-in") {
mainUrlPart += `/adaptive-fit-in`;
}
else if(thumborProps.fit === "full-fit-in") {
mainUrlPart += `/full-fit-in`;
}
}
if(thumborProps.flip) {
if(thumborProps.flip.flipMode === "vertical") {
if(thumborProps.width) {
mainUrlPart += `/${thumborProps.width}x-0`;
}
else if(thumborProps.height) {
mainUrlPart += `/0x-${thumborProps.height}`;
}
else if(thumborProps.width && thumborProps.height) {
mainUrlPart += `/${thumborProps.width}x-${thumborProps.height}`;
}
}
else if(thumborProps.flip.flipMode === "horizontal") {
if(thumborProps.width) {
mainUrlPart += `/-${thumborProps.width}x0`;
}
else if(thumborProps.height) {
mainUrlPart += `/-0x${thumborProps.height}`;
}
else if(thumborProps.width && thumborProps.height) {
mainUrlPart += `/-${thumborProps.width}x${thumborProps.height}`;
}
}
else if(thumborProps.flip.flipMode === "both") {
if(thumborProps.width) {
mainUrlPart += `/-${thumborProps.width}x-0`;
}
else if(thumborProps.height) {
mainUrlPart += `/-0x-${thumborProps.height}`;
}
else if(thumborProps.width && thumborProps.height) {
mainUrlPart += `/-${thumborProps.width}x-${thumborProps.height}`;
}
}
}
else {
if(thumborProps.width && thumborProps.height) {
mainUrlPart += `/${thumborProps.width}x${thumborProps.height}`;
}
else if(thumborProps.width) {
mainUrlPart += `/${thumborProps.width}x`;
}
else if(thumborProps.height) {
mainUrlPart += `/x${thumborProps.height}`;
}
}
if(thumborProps.horizontalAlignment) {
mainUrlPart+=`/${thumborProps.horizontalAlignment}`;
}
if(thumborProps.verticalAlignment) {
mainUrlPart+=`/${thumborProps.verticalAlignment}`;
}
if(thumborProps.smart) {
mainUrlPart+=`/smart`;
}
if(thumborProps.filters && thumborProps.filters.length > 0) {
let sortedFilters = [
...thumborProps.filters.filter(({name}) => name !== 'extract_focal'),
...thumborProps.filters.filter(({name}) => name === 'extract_focal')
];
//console.log('sortedFilters: ', sortedFilters)
let result = buildFiltersString(sortedFilters);
//console.log('result: ', result)
mainUrlPart += result.filterString;;
//console.log('mainUrlPart after filters: ', mainUrlPart)
if(result.extractionProps) {
let cropPart = `${url.replace(/(^\w+:|^)\/\//, '')}`;
let cropMainPart = `/${result.extractionProps.topLeft.x}x${result.extractionProps.topLeft.y}:${result.extractionProps.bottomRight.x}x${result.extractionProps.bottomRight.y}`;
cropMainPart += '/' + encodeURLLocal(srcToUrlLocal(imageSrc));
//console.log('cropMainPart: ', cropMainPart)
//console.log('cropPart: ', cropPart)
if(thumborProps.key && thumborProps.key.length > 0) {
//console.log('to sign: ', mainUrlPart.substring(1, mainUrlPart.length));
let hmacSHA1 = new jsSHA('SHA-1', 'TEXT', { hmacKey: { value: thumborProps.key, format: 'TEXT' } });
let key: any = hmacSHA1.update(cropMainPart.substring(1, cropMainPart.length)).getHMAC('B64');
//key = Base64.stringify(key);
key = key.replace(/\+/g, '-').replace(/\//g, '_');
cropPart+=`/${key}${cropMainPart}`;
}
else {
cropPart+=`/unsafe${cropMainPart}`;
}
mainUrlPart += `/`+encodeURIComponent(cropPart);
//console.log('mainUrlPart after extraction focal crop: ', mainUrlPart)
}
else {
mainUrlPart += `/${encodeURLLocal(srcToUrlLocal(imageSrc))}`;
}
//console.log('mainUrlPart after adding source: ', mainUrlPart)
}
else {
mainUrlPart += `/${encodeURLLocal(srcToUrlLocal(imageSrc))}`;
}
if(thumborProps.key && thumborProps.key.length > 0) {
//console.log('to sign: ', mainUrlPart.substring(1, mainUrlPart.length));
let hmacSHA1 = new jsSHA('SHA-1', 'TEXT', { hmacKey: { value: thumborProps.key, format: 'TEXT' } });
let key: any = hmacSHA1.update(mainUrlPart.substring(1, mainUrlPart.length)).getHMAC('B64');
//key = Base64.stringify(key);
key = key.replace(/\+/g, '-').replace(/\//g, '_');
url+=`/${key}${mainUrlPart}`;
}
else {
url+=`/unsafe${mainUrlPart}`;
}
//console.log('url: ', url);
return new URL(url);
}