transform-image.ts 4.9 KB

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