transform-image.ts 4.4 KB

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