11import { basename , fileExtension , fileExtensionWithSeparator } from 'decap-cms-lib-util' ;
22
3- import type * as Photon from '@silvia-odwyer/photon' ;
43import type { CmsMediaProcessing , EntryField } from '../types/redux' ;
54
6- type PhotonImage = InstanceType < typeof Photon . PhotonImage > ;
7- type PhotonModule = typeof Photon & { default : ( ) => Promise < unknown > } ;
8-
95export type MediaProcessingConfig = {
106 format ?: 'jpeg' | 'webp' ;
117 quality ?: number ;
@@ -191,33 +187,50 @@ function getSourceCrop(sourceWidth: number, sourceHeight: number, aspectRatio?:
191187 return { x : 0 , y : Math . round ( ( sourceHeight - height ) / 2 ) , width : sourceWidth , height } ;
192188}
193189
194- async function getFileBytes ( file : File ) {
195- if ( typeof file . arrayBuffer === 'function' ) {
196- return new Uint8Array ( await file . arrayBuffer ( ) ) ;
190+ function loadImage ( file : File ) {
191+ if ( typeof createImageBitmap === 'function' ) {
192+ return createImageBitmap ( file ) ;
197193 }
198194
199- return new Promise < Uint8Array > ( ( resolve , reject ) => {
200- const reader = new FileReader ( ) ;
201- reader . onload = ( ) => resolve ( new Uint8Array ( reader . result as ArrayBuffer ) ) ;
202- reader . onerror = ( ) => reject ( reader . error ) ;
203- reader . readAsArrayBuffer ( file ) ;
195+ return new Promise < HTMLImageElement > ( ( resolve , reject ) => {
196+ const url = URL . createObjectURL ( file ) ;
197+ const image = new Image ( ) ;
198+ image . onload = ( ) => {
199+ URL . revokeObjectURL ( url ) ;
200+ resolve ( image ) ;
201+ } ;
202+ image . onerror = ( ) => {
203+ URL . revokeObjectURL ( url ) ;
204+ reject ( new Error ( `Failed to load image '${ file . name } '` ) ) ;
205+ } ;
206+ image . src = url ;
204207 } ) ;
205208}
206209
207- function getImageBytes ( image : PhotonImage , format : string , quality = 0.75 ) {
208- if ( format === 'jpeg' ) {
209- return image . get_bytes_jpeg ( Math . round ( quality * 100 ) ) ;
210- }
211-
210+ async function encodeImageData ( imageData : ImageData , format : string , quality = 0.75 ) {
212211 if ( format === 'webp' ) {
213- return image . get_bytes_webp ( ) ;
212+ const { encode } = await import ( '@jsquash/webp' ) ;
213+ return encode ( imageData , { quality : Math . round ( quality * 100 ) } ) ;
214214 }
215215
216- if ( format === 'png' ) {
217- return image . get_bytes ( ) ;
218- }
216+ throw new Error ( `ImageData encoding is not supported for '${ format } '` ) ;
217+ }
219218
220- throw new Error ( `Photon does not support encoding images as '${ format } '` ) ;
219+ function encodeCanvas ( canvas : HTMLCanvasElement , format : string , quality = 0.75 ) {
220+ return new Promise < Blob > ( ( resolve , reject ) => {
221+ canvas . toBlob (
222+ blob => {
223+ if ( ! blob ) {
224+ reject ( new Error ( `Failed to encode image as '${ format } '` ) ) ;
225+ return ;
226+ }
227+
228+ resolve ( blob ) ;
229+ } ,
230+ getMimeType ( format ) ,
231+ quality ,
232+ ) ;
233+ } ) ;
221234}
222235
223236export async function transformImage (
@@ -227,42 +240,34 @@ export async function transformImage(
227240) : Promise < ImageTransformationFile [ ] > {
228241 const outputFormat = getOutputFormat ( file . name , config ) ;
229242 const mimeType = getMimeType ( outputFormat ) ;
230- const photon = ( await import ( '@silvia-odwyer/photon' ) ) as unknown as PhotonModule ;
231- await photon . default ( ) ;
232-
233- const imageBytes = await getFileBytes ( file ) ;
234- const originalImage = photon . PhotonImage . new_from_byteslice ( imageBytes ) ;
235- const imagesToFree : PhotonImage [ ] = [ originalImage ] ;
236-
237- try {
238- const sourceWidth = originalImage . get_width ( ) ;
239- const sourceHeight = originalImage . get_height ( ) ;
240- const crop = getSourceCrop ( sourceWidth , sourceHeight , config . aspectRatio ) ;
241- const croppedImage =
242- crop . x === 0 && crop . y === 0 && crop . width === sourceWidth && crop . height === sourceHeight
243- ? originalImage
244- : photon . crop ( originalImage , crop . x , crop . y , crop . x + crop . width , crop . y + crop . height ) ;
245-
246- if ( croppedImage !== originalImage ) {
247- imagesToFree . push ( croppedImage ) ;
248- }
249-
250- const { width, height } = getTargetDimensions ( crop . width , crop . height , config ) ;
251- const processedImage =
252- width === croppedImage . get_width ( ) && height === croppedImage . get_height ( )
253- ? croppedImage
254- : photon . resize ( croppedImage , width , height , photon . SamplingFilter . Lanczos3 ) ;
255-
256- if ( processedImage !== croppedImage ) {
257- imagesToFree . push ( processedImage ) ;
258- }
259-
260- const bytes = getImageBytes ( processedImage , outputFormat , config . quality ) ;
261- const fileName = getMediaProcessingFileName ( file . name , config ) ;
262- const transformedFile = new File ( [ bytes ] , fileName , { type : mimeType } ) ;
263-
264- return [ { file : transformedFile , path : getProcessedPath ( originalPath , fileName ) } ] ;
265- } finally {
266- imagesToFree . forEach ( image => image . free ( ) ) ;
243+ const image = await loadImage ( file ) ;
244+ const crop = getSourceCrop ( image . width , image . height , config . aspectRatio ) ;
245+ const { width, height } = getTargetDimensions ( crop . width , crop . height , config ) ;
246+ const canvas = document . createElement ( 'canvas' ) ;
247+ const context = canvas . getContext ( '2d' ) ;
248+
249+ if ( ! context ) {
250+ throw new Error ( 'Canvas 2D context is not available' ) ;
267251 }
252+
253+ canvas . width = width ;
254+ canvas . height = height ;
255+ context . drawImage ( image , crop . x , crop . y , crop . width , crop . height , 0 , 0 , width , height ) ;
256+
257+ if ( 'close' in image && typeof image . close === 'function' ) {
258+ image . close ( ) ;
259+ }
260+
261+ const encoded =
262+ outputFormat === 'webp'
263+ ? await encodeImageData (
264+ context . getImageData ( 0 , 0 , width , height ) ,
265+ outputFormat ,
266+ config . quality ,
267+ )
268+ : await encodeCanvas ( canvas , outputFormat , config . quality ) ;
269+ const fileName = getMediaProcessingFileName ( file . name , config ) ;
270+ const transformedFile = new File ( [ encoded ] , fileName , { type : mimeType } ) ;
271+
272+ return [ { file : transformedFile , path : getProcessedPath ( originalPath , fileName ) } ] ;
268273}
0 commit comments