transform-image.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. import sharp, { Region, ResizeOptions } from 'sharp';
  2. import { ImageTransformPreset } from './types';
  3. export type Dimensions = { w: number; h: number };
  4. export type Point = { x: number; y: number };
  5. /**
  6. * Applies transforms to the given image according to the query params passed.
  7. */
  8. export async function transformImage(
  9. originalImage: Buffer,
  10. queryParams: Record<string, string>,
  11. presets: ImageTransformPreset[],
  12. ): Promise<sharp.Sharp> {
  13. let targetWidth = Math.round(+queryParams.w) || undefined;
  14. let targetHeight = Math.round(+queryParams.h) || undefined;
  15. let mode = queryParams.mode || 'crop';
  16. const fpx = +queryParams.fpx || undefined;
  17. const fpy = +queryParams.fpy || undefined;
  18. if (queryParams.preset) {
  19. const matchingPreset = presets.find(p => p.name === queryParams.preset);
  20. if (matchingPreset) {
  21. targetWidth = matchingPreset.width;
  22. targetHeight = matchingPreset.height;
  23. mode = matchingPreset.mode;
  24. }
  25. }
  26. const options: ResizeOptions = {};
  27. if (mode === 'crop') {
  28. options.position = sharp.strategy.entropy;
  29. } else {
  30. options.fit = 'inside';
  31. }
  32. const image = sharp(originalImage);
  33. if (fpx && fpy && targetWidth && targetHeight && mode === 'crop') {
  34. const metadata = await image.metadata();
  35. if (metadata.width && metadata.height) {
  36. const xCenter = fpx * metadata.width;
  37. const yCenter = fpy * metadata.height;
  38. const { width, height, region } = resizeToFocalPoint(
  39. { w: metadata.width, h: metadata.height },
  40. { w: targetWidth, h: targetHeight },
  41. { x: xCenter, y: yCenter },
  42. );
  43. return image.resize(width, height).extract(region);
  44. }
  45. }
  46. return image.resize(targetWidth, targetHeight, options);
  47. }
  48. /**
  49. * Resize an image but keep it centered on the focal point.
  50. * Based on the method outlined in https://github.com/lovell/sharp/issues/1198#issuecomment-384591756
  51. */
  52. export function resizeToFocalPoint(
  53. original: Dimensions,
  54. target: Dimensions,
  55. focalPoint: Point,
  56. ): { width: number; height: number; region: Region } {
  57. const { width, height, factor } = getIntermediateDimensions(original, target);
  58. const region = getExtractionRegion(factor, focalPoint, target, { w: width, h: height });
  59. return { width, height, region };
  60. }
  61. /**
  62. * Calculates the dimensions of the intermediate (resized) image.
  63. */
  64. function getIntermediateDimensions(
  65. original: Dimensions,
  66. target: Dimensions,
  67. ): { width: number; height: number; factor: number } {
  68. const hRatio = original.h / target.h;
  69. const wRatio = original.w / target.w;
  70. let factor: number;
  71. let width: number;
  72. let height: number;
  73. if (hRatio < wRatio) {
  74. factor = hRatio;
  75. height = Math.round(target.h);
  76. width = Math.round(original.w / factor);
  77. } else {
  78. factor = wRatio;
  79. width = Math.round(target.w);
  80. height = Math.round(original.h / factor);
  81. }
  82. return { width, height, factor };
  83. }
  84. /**
  85. * Calculates the Region to extract from the intermediate image.
  86. */
  87. function getExtractionRegion(
  88. factor: number,
  89. focalPoint: Point,
  90. target: Dimensions,
  91. intermediate: Dimensions,
  92. ): Region {
  93. const newXCenter = focalPoint.x / factor;
  94. const newYCenter = focalPoint.y / factor;
  95. const region: Region = {
  96. left: 0,
  97. top: 0,
  98. width: target.w,
  99. height: target.h,
  100. };
  101. if (intermediate.h < intermediate.w) {
  102. region.left = clamp(0, intermediate.w - target.w, Math.round(newXCenter - target.w / 2));
  103. } else {
  104. region.top = clamp(0, intermediate.h - target.h, Math.round(newYCenter - target.h / 2));
  105. }
  106. return region;
  107. }
  108. /**
  109. * Limit the input value to the specified min and max values.
  110. */
  111. function clamp(min: number, max: number, input: number) {
  112. return Math.min(Math.max(min, input), max);
  113. }