@sanity/image-url
Version:
Tools to generate image urls from Sanity content
611 lines (486 loc) • 17.3 kB
JavaScript
(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