sharp-asset-preview-strategy.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. import { AssetType } from '@vendure/common/lib/generated-types';
  2. import { AssetPreviewStrategy, getAssetType, Logger, RequestContext } from '@vendure/core';
  3. import path from 'path';
  4. import sharp from 'sharp';
  5. import { loggerCtx } from './constants';
  6. /**
  7. * @description
  8. * This {@link AssetPreviewStrategy} uses the [Sharp library](https://sharp.pixelplumbing.com/) to generate
  9. * preview images of uploaded binary files. For non-image binaries, a generic "file" icon with the mime type
  10. * overlay will be generated.
  11. *
  12. * @docsCategory core plugins/AssetServerPlugin
  13. * @docsPage SharpAssetPreviewStrategy
  14. */
  15. interface SharpAssetPreviewConfig {
  16. /**
  17. * @description
  18. * The max height in pixels of a generated preview image.
  19. *
  20. * @default 1600
  21. */
  22. maxHeight?: number;
  23. /**
  24. * @description
  25. * The max width in pixels of a generated preview image.
  26. *
  27. * @default 1600
  28. */
  29. maxWidth?: number;
  30. /**
  31. * @description
  32. * Set Sharp's options for encoding jpeg files: https://sharp.pixelplumbing.com/api-output#jpeg
  33. *
  34. * @since 1.7.0
  35. */
  36. jpegOptions?: sharp.JpegOptions;
  37. /**
  38. * @description
  39. * Set Sharp's options for encoding png files: https://sharp.pixelplumbing.com/api-output#png
  40. *
  41. * @since 1.7.0
  42. */
  43. pngOptions?: sharp.PngOptions;
  44. /**
  45. * @description
  46. * Set Sharp's options for encoding webp files: https://sharp.pixelplumbing.com/api-output#webp
  47. *
  48. * @since 1.7.0
  49. */
  50. webpOptions?: sharp.WebpOptions;
  51. /**
  52. * @description
  53. * Set Sharp's options for encoding gif files: https://sharp.pixelplumbing.com/api-output#gif
  54. *
  55. * @since 1.7.0
  56. */
  57. gifOptions?: sharp.GifOptions;
  58. /**
  59. * @description
  60. * Set Sharp's options for encoding avif files: https://sharp.pixelplumbing.com/api-output#avif
  61. *
  62. * @since 1.7.0
  63. */
  64. avifOptions?: sharp.AvifOptions;
  65. }
  66. /**
  67. * @description
  68. * This {@link AssetPreviewStrategy} uses the [Sharp library](https://sharp.pixelplumbing.com/) to generate
  69. * preview images of uploaded binary files. For non-image binaries, a generic "file" icon with the mime type
  70. * overlay will be generated.
  71. *
  72. * By default, this strategy will produce previews up to maximum dimensions of 1600 x 1600 pixels. The created
  73. * preview images will match the input format - so a source file in jpeg format will output a jpeg preview,
  74. * a webp source file will output a webp preview, and so on.
  75. *
  76. * The settings for the outputs will default to Sharp's defaults (https://sharp.pixelplumbing.com/api-output).
  77. * However, it is possible to pass your own configurations to control the output of each format:
  78. *
  79. * ```ts
  80. * AssetServerPlugin.init({
  81. * previewStrategy: new SharpAssetPreviewStrategy({
  82. * jpegOptions: { quality: 95 },
  83. * webpOptions: { quality: 95 },
  84. * }),
  85. * }),
  86. * ```
  87. *
  88. * @docsCategory core plugins/AssetServerPlugin
  89. * @docsPage SharpAssetPreviewStrategy
  90. * @docsWeight 0
  91. */
  92. export class SharpAssetPreviewStrategy implements AssetPreviewStrategy {
  93. private readonly defaultConfig: Required<SharpAssetPreviewConfig> = {
  94. maxHeight: 1600,
  95. maxWidth: 1600,
  96. jpegOptions: {},
  97. pngOptions: {},
  98. webpOptions: {},
  99. gifOptions: {},
  100. avifOptions: {},
  101. };
  102. private readonly config: Required<SharpAssetPreviewConfig>;
  103. constructor(config?: SharpAssetPreviewConfig) {
  104. this.config = {
  105. ...this.defaultConfig,
  106. ...(config ?? {}),
  107. };
  108. }
  109. async generatePreviewImage(ctx: RequestContext, mimeType: string, data: Buffer): Promise<Buffer> {
  110. const assetType = getAssetType(mimeType);
  111. const { maxWidth, maxHeight } = this.config;
  112. if (assetType === AssetType.IMAGE) {
  113. try {
  114. const image = sharp(data, { failOn: 'truncated' }).rotate();
  115. const metadata = await image.metadata();
  116. const width = metadata.width || 0;
  117. const height = metadata.height || 0;
  118. if (maxWidth < width || maxHeight < height) {
  119. image.resize(maxWidth, maxHeight, { fit: 'inside' });
  120. }
  121. if (mimeType === 'image/svg+xml') {
  122. // Convert the SVG to a raster for the preview
  123. return image.toBuffer();
  124. } else {
  125. switch (metadata.format) {
  126. case 'jpeg':
  127. case 'jpg':
  128. return image.jpeg(this.config.jpegOptions).toBuffer();
  129. case 'png':
  130. return image.png(this.config.pngOptions).toBuffer();
  131. case 'webp':
  132. return image.webp(this.config.webpOptions).toBuffer();
  133. case 'gif':
  134. return image.gif(this.config.jpegOptions).toBuffer();
  135. case 'avif':
  136. return image.avif(this.config.avifOptions).toBuffer();
  137. default:
  138. return image.toBuffer();
  139. }
  140. }
  141. } catch (err: any) {
  142. Logger.error(
  143. `An error occurred when generating preview for image with mimeType ${mimeType}: ${JSON.stringify(
  144. err.message,
  145. )}`,
  146. loggerCtx,
  147. );
  148. return this.generateBinaryFilePreview(mimeType);
  149. }
  150. } else {
  151. return this.generateBinaryFilePreview(mimeType);
  152. }
  153. }
  154. private generateMimeTypeOverlay(mimeType: string): Buffer {
  155. return Buffer.from(`
  156. <svg xmlns="http://www.w3.org/2000/svg" height="150" width="800">
  157. <style>
  158. text {
  159. font-size: 64px;
  160. font-family: Arial, sans-serif;
  161. fill: #666;
  162. }
  163. </style>
  164. <text x="400" y="110" text-anchor="middle" width="800">${mimeType}</text>
  165. </svg>`);
  166. }
  167. private generateBinaryFilePreview(mimeType: string): Promise<Buffer> {
  168. return sharp(path.join(__dirname, 'file-icon.png'))
  169. .resize(800, 800, { fit: 'outside' })
  170. .composite([
  171. {
  172. input: this.generateMimeTypeOverlay(mimeType),
  173. gravity: sharp.gravity.center,
  174. },
  175. ])
  176. .toBuffer();
  177. }
  178. }