UNPKG

react-thumbor-image

Version:

Makes it easy to add Thumbor optimized images to your React app.

511 lines (453 loc) 18.6 kB
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); }