UNPKG

@sanity/image-url

Version:

Tools to generate image urls from Sanity content

611 lines (486 loc) 17.3 kB
(function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global.SanityImageUrlBuilder = factory()); }(this, (function () { var example = 'image-Tb9Ew8CXIwaY6R1kjMvI0uRR-2000x3000-jpg'; function parseAssetId(ref) { var ref$1 = ref.split('-'); var id = ref$1[1]; var dimensionString = ref$1[2]; var format = ref$1[3]; if (!id || !dimensionString || !format) { throw new Error(("Malformed asset _ref '" + ref + "'. Expected an id like \"" + example + "\".")); } var ref$2 = dimensionString.split('x'); var imgWidthStr = ref$2[0]; var imgHeightStr = ref$2[1]; var width = +imgWidthStr; var height = +imgHeightStr; var isValidAssetId = isFinite(width) && isFinite(height); if (!isValidAssetId) { throw new Error(("Malformed asset _ref '" + ref + "'. Expected an id like \"" + example + "\".")); } return { id: id, width: width, height: height, format: format }; } var isRef = function (src) { var source = src; return source ? typeof source._ref === 'string' : false; }; var isAsset = function (src) { var source = src; return source ? typeof source._id === 'string' : false; }; var isAssetStub = function (src) { var source = src; return source && source.asset ? typeof source.asset.url === 'string' : false; }; // Convert an asset-id, asset or image to an image record suitable for processing // eslint-disable-next-line complexity function parseSource(source) { if (!source) { return null; } var image; if (typeof source === 'string' && isUrl(source)) { // Someone passed an existing image url? image = { asset: { _ref: urlToId(source) } }; } else if (typeof source === 'string') { // Just an asset id image = { asset: { _ref: source } }; } else if (isRef(source)) { // We just got passed an asset directly image = { asset: source }; } else if (isAsset(source)) { // If we were passed an image asset document image = { asset: { _ref: source._id || '' } }; } else if (isAssetStub(source)) { // If we were passed a partial asset (`url`, but no `_id`) image = { asset: { _ref: urlToId(source.asset.url) } }; } else if (typeof source.asset === 'object') { // Probably an actual image with materialized asset image = source; } else { // We got something that does not look like an image, or it is an image // that currently isn't sporting an asset. return null; } var img = source; if (img.crop) { image.crop = img.crop; } if (img.hotspot) { image.hotspot = img.hotspot; } return applyDefaults(image); } function isUrl(url) { return /^https?:\/\//.test(("" + url)); } function urlToId(url) { var parts = url.split('/').slice(-1); return ("image-" + (parts[0])).replace(/\.([a-z]+)$/, '-$1'); } // Mock crop and hotspot if image lacks it function applyDefaults(image) { if (image.crop && image.hotspot) { return image; } // We need to pad in default values for crop or hotspot var result = Object.assign({}, image); if (!result.crop) { result.crop = { left: 0, top: 0, bottom: 0, right: 0 }; } if (!result.hotspot) { result.hotspot = { x: 0.5, y: 0.5, height: 1.0, width: 1.0 }; } return result; } var SPEC_NAME_TO_URL_NAME_MAPPINGS = [['width', 'w'], ['height', 'h'], ['format', 'fm'], ['download', 'dl'], ['blur', 'blur'], ['sharpen', 'sharp'], ['invert', 'invert'], ['orientation', 'or'], ['minHeight', 'min-h'], ['maxHeight', 'max-h'], ['minWidth', 'min-w'], ['maxWidth', 'max-w'], ['quality', 'q'], ['fit', 'fit'], ['crop', 'crop'], ['auto', 'auto'], ['dpr', 'dpr']]; function urlForImage(options) { var spec = Object.assign({}, (options || {})); var source = spec.source; delete spec.source; var image = parseSource(source); if (!image) { return null; } var id = image.asset._ref || image.asset._id || ''; var asset = parseAssetId(id); // Compute crop rect in terms of pixel coordinates in the raw source image var cropLeft = Math.round(image.crop.left * asset.width); var cropTop = Math.round(image.crop.top * asset.height); var crop = { left: cropLeft, top: cropTop, width: Math.round(asset.width - image.crop.right * asset.width - cropLeft), height: Math.round(asset.height - image.crop.bottom * asset.height - cropTop) }; // Compute hot spot rect in terms of pixel coordinates var hotSpotVerticalRadius = image.hotspot.height * asset.height / 2; var hotSpotHorizontalRadius = image.hotspot.width * asset.width / 2; var hotSpotCenterX = image.hotspot.x * asset.width; var hotSpotCenterY = image.hotspot.y * asset.height; var hotspot = { left: hotSpotCenterX - hotSpotHorizontalRadius, top: hotSpotCenterY - hotSpotVerticalRadius, right: hotSpotCenterX + hotSpotHorizontalRadius, bottom: hotSpotCenterY + hotSpotVerticalRadius }; // If irrelevant, or if we are requested to: don't perform crop/fit based on // the crop/hotspot. if (!(spec.rect || spec.focalPoint || spec.ignoreImageParams || spec.crop)) { spec = Object.assign({}, spec, fit({ crop: crop, hotspot: hotspot }, spec)); } return specToImageUrl(Object.assign({}, spec, {asset: asset})); } // eslint-disable-next-line complexity function specToImageUrl(spec) { var cdnUrl = spec.baseUrl || 'https://cdn.sanity.io'; var filename = (spec.asset.id) + "-" + (spec.asset.width) + "x" + (spec.asset.height) + "." + (spec.asset.format); var baseUrl = cdnUrl + "/images/" + (spec.projectId) + "/" + (spec.dataset) + "/" + filename; var params = []; if (spec.rect) { // Only bother url with a crop if it actually crops anything var ref = spec.rect; var left = ref.left; var top = ref.top; var width = ref.width; var height = ref.height; var isEffectiveCrop = left !== 0 || top !== 0 || height !== spec.asset.height || width !== spec.asset.width; if (isEffectiveCrop) { params.push(("rect=" + left + "," + top + "," + width + "," + height)); } } if (spec.bg) { params.push(("bg=" + (spec.bg))); } if (spec.focalPoint) { params.push(("fp-x=" + (spec.focalPoint.x))); params.push(("fp-x=" + (spec.focalPoint.y))); } var flip = [spec.flipHorizontal && 'h', spec.flipVertical && 'v'].filter(Boolean).join(''); if (flip) { params.push(("flip=" + flip)); } // Map from spec name to url param name, and allow using the actual param name as an alternative SPEC_NAME_TO_URL_NAME_MAPPINGS.forEach(function (mapping) { var specName = mapping[0]; var param = mapping[1]; if (typeof spec[specName] !== 'undefined') { params.push((param + "=" + (encodeURIComponent(spec[specName])))); } else if (typeof spec[param] !== 'undefined') { params.push((param + "=" + (encodeURIComponent(spec[param])))); } }); if (params.length === 0) { return baseUrl; } return (baseUrl + "?" + (params.join('&'))); } function fit(source, spec) { var cropRect; var imgWidth = spec.width; var imgHeight = spec.height; // If we are not constraining the aspect ratio, we'll just use the whole crop if (!(imgWidth && imgHeight)) { return { width: imgWidth, height: imgHeight, rect: source.crop }; } var crop = source.crop; var hotspot = source.hotspot; // If we are here, that means aspect ratio is locked and fitting will be a bit harder var desiredAspectRatio = imgWidth / imgHeight; var cropAspectRatio = crop.width / crop.height; if (cropAspectRatio > desiredAspectRatio) { // The crop is wider than the desired aspect ratio. That means we are cutting from the sides var height = crop.height; var width = height * desiredAspectRatio; var top = crop.top; // Center output horizontally over hotspot var hotspotXCenter = (hotspot.right - hotspot.left) / 2 + hotspot.left; var left = hotspotXCenter - width / 2; // Keep output within crop if (left < crop.left) { left = crop.left; } else if (left + width > crop.left + crop.width) { left = crop.left + crop.width - width; } cropRect = { left: Math.round(left), top: Math.round(top), width: Math.round(width), height: Math.round(height) }; } else { // The crop is taller than the desired ratio, we are cutting from top and bottom var width$1 = crop.width; var height$1 = width$1 / desiredAspectRatio; var left$1 = crop.left; // Center output vertically over hotspot var hotspotYCenter = (hotspot.bottom - hotspot.top) / 2 + hotspot.top; var top$1 = hotspotYCenter - height$1 / 2; // Keep output rect within crop if (top$1 < crop.top) { top$1 = crop.top; } else if (top$1 + height$1 > crop.top + crop.height) { top$1 = crop.top + crop.height - height$1; } cropRect = { left: Math.max(0, Math.floor(left$1)), top: Math.max(0, Math.floor(top$1)), width: Math.round(width$1), height: Math.round(height$1) }; } return { width: imgWidth, height: imgHeight, rect: cropRect }; } // For backwards-compatibility var validFits = ['clip', 'crop', 'fill', 'fillmax', 'max', 'scale', 'min']; var validCrops = ['top', 'bottom', 'left', 'right', 'center', 'focalpoint', 'entropy']; var validAutoModes = ['format']; function isSanityClient(client) { return client ? typeof client.clientConfig === 'object' : false; } function rewriteSpecName(key) { var specs = SPEC_NAME_TO_URL_NAME_MAPPINGS; for (var i = 0, list = specs; i < list.length; i += 1) { var entry = list[i]; var specName = entry[0]; var param = entry[1]; if (key === specName || key === param) { return specName; } } return key; } function urlBuilder(options) { // Did we get a SanityClient? var client = options; if (isSanityClient(client)) { // Inherit config from client var ref = client.clientConfig; var apiHost = ref.apiHost; var projectId = ref.projectId; var dataset = ref.dataset; return new ImageUrlBuilder(null, { baseUrl: apiHost.replace(/^https:\/\/api\./, 'https://cdn.'), projectId: projectId, dataset: dataset }); } // Or just accept the options as given return new ImageUrlBuilder(null, options); } var ImageUrlBuilder = function ImageUrlBuilder(parent, options) { this.options = parent ? Object.assign({}, (parent.options || {}), (options || {})) // Merge parent options : Object.assign({}, (options || {})); // Copy options }; ImageUrlBuilder.prototype.withOptions = function withOptions (options) { var baseUrl = options.baseUrl || ''; var newOptions = { baseUrl: baseUrl }; for (var key in options) { if (options.hasOwnProperty(key)) { var specKey = rewriteSpecName(key); newOptions[specKey] = options[key]; } } return new ImageUrlBuilder(this, Object.assign({}, {baseUrl: baseUrl}, newOptions)); }; // The image to be represented. Accepts a Sanity 'image'-document, 'asset'-document or // _id of asset. To get the benefit of automatic hot-spot/crop integration with the content // studio, the 'image'-document must be provided. ImageUrlBuilder.prototype.image = function image (source) { return this.withOptions({ source: source }); }; // Specify the dataset ImageUrlBuilder.prototype.dataset = function dataset (dataset$1) { return this.withOptions({ dataset: dataset$1 }); }; // Specify the projectId ImageUrlBuilder.prototype.projectId = function projectId (projectId$1) { return this.withOptions({ projectId: projectId$1 }); }; // Specify background color ImageUrlBuilder.prototype.bg = function bg (bg$1) { return this.withOptions({ bg: bg$1 }); }; // Set DPR scaling factor ImageUrlBuilder.prototype.dpr = function dpr (dpr$1) { return this.withOptions({ dpr: dpr$1 }); }; // Specify the width of the image in pixels ImageUrlBuilder.prototype.width = function width (width$1) { return this.withOptions({ width: width$1 }); }; // Specify the height of the image in pixels ImageUrlBuilder.prototype.height = function height (height$1) { return this.withOptions({ height: height$1 }); }; // Specify focal point in fraction of image dimensions. Each component 0.0-1.0 ImageUrlBuilder.prototype.focalPoint = function focalPoint (x, y) { return this.withOptions({ focalPoint: { x: x, y: y } }); }; ImageUrlBuilder.prototype.maxWidth = function maxWidth (maxWidth$1) { return this.withOptions({ maxWidth: maxWidth$1 }); }; ImageUrlBuilder.prototype.minWidth = function minWidth (minWidth$1) { return this.withOptions({ minWidth: minWidth$1 }); }; ImageUrlBuilder.prototype.maxHeight = function maxHeight (maxHeight$1) { return this.withOptions({ maxHeight: maxHeight$1 }); }; ImageUrlBuilder.prototype.minHeight = function minHeight (minHeight$1) { return this.withOptions({ minHeight: minHeight$1 }); }; // Specify width and height in pixels ImageUrlBuilder.prototype.size = function size (width, height) { return this.withOptions({ width: width, height: height }); }; // Specify blur between 0 and 100 ImageUrlBuilder.prototype.blur = function blur (blur$1) { return this.withOptions({ blur: blur$1 }); }; ImageUrlBuilder.prototype.sharpen = function sharpen (sharpen$1) { return this.withOptions({ sharpen: sharpen$1 }); }; // Specify the desired rectangle of the image ImageUrlBuilder.prototype.rect = function rect (left, top, width, height) { return this.withOptions({ rect: { left: left, top: top, width: width, height: height } }); }; // Specify the image format of the image. 'jpg', 'pjpg', 'png', 'webp' ImageUrlBuilder.prototype.format = function format (format$1) { return this.withOptions({ format: format$1 }); }; ImageUrlBuilder.prototype.invert = function invert (invert$1) { return this.withOptions({ invert: invert$1 }); }; // Rotation in degrees 0, 90, 180, 270 ImageUrlBuilder.prototype.orientation = function orientation (orientation$1) { return this.withOptions({ orientation: orientation$1 }); }; // Compression quality 0-100 ImageUrlBuilder.prototype.quality = function quality (quality$1) { return this.withOptions({ quality: quality$1 }); }; // Make it a download link. Parameter is default filename. ImageUrlBuilder.prototype.forceDownload = function forceDownload (download) { return this.withOptions({ download: download }); }; // Flip image horizontally ImageUrlBuilder.prototype.flipHorizontal = function flipHorizontal () { return this.withOptions({ flipHorizontal: true }); }; // Flip image verically ImageUrlBuilder.prototype.flipVertical = function flipVertical () { return this.withOptions({ flipVertical: true }); }; // Ignore crop/hotspot from image record, even when present ImageUrlBuilder.prototype.ignoreImageParams = function ignoreImageParams () { return this.withOptions({ ignoreImageParams: true }); }; ImageUrlBuilder.prototype.fit = function fit (value) { if (validFits.indexOf(value) === -1) { throw new Error(("Invalid fit mode \"" + value + "\"")); } return this.withOptions({ fit: value }); }; ImageUrlBuilder.prototype.crop = function crop (value) { if (validCrops.indexOf(value) === -1) { throw new Error(("Invalid crop mode \"" + value + "\"")); } return this.withOptions({ crop: value }); }; ImageUrlBuilder.prototype.auto = function auto (value) { if (validAutoModes.indexOf(value) === -1) { throw new Error(("Invalid auto mode \"" + value + "\"")); } return this.withOptions({ auto: value }); }; // Gets the url based on the submitted parameters ImageUrlBuilder.prototype.url = function url () { return urlForImage(this.options); }; // Synonym for url() ImageUrlBuilder.prototype.toString = function toString () { return this.url(); }; return urlBuilder; }))); //# sourceMappingURL=image-url.umd.js.map