@wordpress/upload-media
Version:
Core media upload logic.
8 lines (7 loc) • 13.7 kB
Source Map (JSON)
{
"version": 3,
"sources": ["../src/canvas-utils.ts"],
"sourcesContent": ["/**\n * Internal dependencies\n */\nimport { getFileBasename } from './utils';\nimport { parseHeic } from './heic-parser';\n\n/**\n * Converts an image file to JPEG using the browser's native decoder and canvas.\n *\n * Tries three decoding strategies:\n * 1. createImageBitmap() + OffscreenCanvas (works in Safari, future Chrome).\n * 2. WebCodecs ImageDecoder API (uses platform codecs; may work in future\n * Chrome if HEIC is added to its image decoder pipeline).\n * 3. HEIC container parsing + WebCodecs VideoDecoder (Chrome 107+ on macOS).\n * Parses the HEIC/ISOBMFF container to extract the HEVC bitstream, then\n * decodes it using Chrome's platform HEVC video decoder (hardware-\n * accelerated via macOS VideoToolbox).\n *\n * This avoids shipping our own HEVC decoder, sidestepping patent/licensing concerns.\n *\n * @param file Source image file (e.g., HEIC/HEIF).\n * @param quality JPEG quality (0-1). Default 0.82.\n * @return JPEG File object.\n */\nexport async function canvasConvertToJpeg(\n\tfile: File,\n\tquality = 0.82\n): Promise< File > {\n\tconst baseName = getFileBasename( file.name );\n\n\t// Strategy 1: createImageBitmap + OffscreenCanvas.\n\ttry {\n\t\tconst bitmap = await createImageBitmap( file );\n\t\ttry {\n\t\t\tconst canvas = new OffscreenCanvas( bitmap.width, bitmap.height );\n\t\t\tconst ctx = canvas.getContext( '2d' );\n\n\t\t\tif ( ! ctx ) {\n\t\t\t\tthrow new Error( 'Could not get canvas 2d context' );\n\t\t\t}\n\n\t\t\tctx.drawImage( bitmap, 0, 0 );\n\n\t\t\tconst jpegBlob = await canvas.convertToBlob( {\n\t\t\t\ttype: 'image/jpeg',\n\t\t\t\tquality,\n\t\t\t} );\n\n\t\t\treturn new File( [ jpegBlob ], `${ baseName }.jpg`, {\n\t\t\t\ttype: 'image/jpeg',\n\t\t\t} );\n\t\t} finally {\n\t\t\tbitmap.close();\n\t\t}\n\t} catch {\n\t\t// createImageBitmap doesn't support HEIC in this browser.\n\t\t// Fall through to strategy 2.\n\t}\n\n\t// Strategy 2: WebCodecs ImageDecoder API.\n\t// Uses platform codecs (e.g., macOS HEIC support) that may not be\n\t// exposed through createImageBitmap or <img> elements.\n\tif ( typeof ImageDecoder !== 'undefined' ) {\n\t\tconst supported = await ImageDecoder.isTypeSupported( file.type );\n\t\tif ( supported ) {\n\t\t\tconst decoder = new ImageDecoder( {\n\t\t\t\ttype: file.type,\n\t\t\t\tdata: file.stream(),\n\t\t\t} );\n\t\t\ttry {\n\t\t\t\tconst { image: videoFrame } = await decoder.decode();\n\t\t\t\ttry {\n\t\t\t\t\tconst canvas = new OffscreenCanvas(\n\t\t\t\t\t\tvideoFrame.displayWidth,\n\t\t\t\t\t\tvideoFrame.displayHeight\n\t\t\t\t\t);\n\t\t\t\t\tconst ctx = canvas.getContext( '2d' );\n\n\t\t\t\t\tif ( ! ctx ) {\n\t\t\t\t\t\tthrow new Error( 'Could not get canvas 2d context' );\n\t\t\t\t\t}\n\n\t\t\t\t\tctx.drawImage( videoFrame, 0, 0 );\n\n\t\t\t\t\tconst jpegBlob = await canvas.convertToBlob( {\n\t\t\t\t\t\ttype: 'image/jpeg',\n\t\t\t\t\t\tquality,\n\t\t\t\t\t} );\n\n\t\t\t\t\treturn new File( [ jpegBlob ], `${ baseName }.jpg`, {\n\t\t\t\t\t\ttype: 'image/jpeg',\n\t\t\t\t\t} );\n\t\t\t\t} finally {\n\t\t\t\t\tvideoFrame.close();\n\t\t\t\t}\n\t\t\t} finally {\n\t\t\t\tdecoder.close();\n\t\t\t}\n\t\t}\n\t}\n\n\t// Strategy 3: HEIC container parsing + WebCodecs VideoDecoder.\n\t// Chrome 107+ on macOS supports HEVC *video* decoding via platform codecs\n\t// (macOS VideoToolbox), even though it doesn't support HEIC through image\n\t// APIs. A HEIC file is an ISOBMFF container with HEVC-encoded tiles —\n\t// we parse the container and decode each tile via VideoDecoder.\n\tif ( typeof VideoDecoder !== 'undefined' ) {\n\t\ttry {\n\t\t\tconst heicData = parseHeic( await file.arrayBuffer() );\n\n\t\t\tconst support = await VideoDecoder.isConfigSupported( {\n\t\t\t\tcodec: heicData.codecString,\n\t\t\t} );\n\n\t\t\tif ( support.supported ) {\n\t\t\t\tconst canvas = new OffscreenCanvas(\n\t\t\t\t\theicData.outputWidth,\n\t\t\t\t\theicData.outputHeight\n\t\t\t\t);\n\t\t\t\tconst ctx = canvas.getContext( '2d' );\n\n\t\t\t\tif ( ! ctx ) {\n\t\t\t\t\tthrow new Error( 'Could not get canvas 2d context' );\n\t\t\t\t}\n\n\t\t\t\t// Decode each tile and draw it at its grid position.\n\t\t\t\tfor ( const tile of heicData.tiles ) {\n\t\t\t\t\tconst frame = await decodeHevcFrame(\n\t\t\t\t\t\theicData.codecString,\n\t\t\t\t\t\theicData.description,\n\t\t\t\t\t\theicData.tileWidth,\n\t\t\t\t\t\theicData.tileHeight,\n\t\t\t\t\t\ttile.data\n\t\t\t\t\t);\n\t\t\t\t\ttry {\n\t\t\t\t\t\tctx.drawImage( frame, tile.x, tile.y );\n\t\t\t\t\t} finally {\n\t\t\t\t\t\tframe.close();\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Apply orientation: the native ISOBMFF `irot` transform when\n\t\t\t\t// present, otherwise the EXIF orientation tag (libheif applies\n\t\t\t\t// the former but ignores the latter for HEIF-family inputs).\n\t\t\t\tconst outputCanvas =\n\t\t\t\t\theicData.rotation !== 0\n\t\t\t\t\t\t? applyRotation( canvas, heicData.rotation )\n\t\t\t\t\t\t: applyExifOrientation(\n\t\t\t\t\t\t\t\tcanvas,\n\t\t\t\t\t\t\t\theicData.exifOrientation\n\t\t\t\t\t\t );\n\n\t\t\t\tconst jpegBlob = await outputCanvas.convertToBlob( {\n\t\t\t\t\ttype: 'image/jpeg',\n\t\t\t\t\tquality,\n\t\t\t\t} );\n\n\t\t\t\treturn new File( [ jpegBlob ], `${ baseName }.jpg`, {\n\t\t\t\t\ttype: 'image/jpeg',\n\t\t\t\t} );\n\t\t\t}\n\t\t} catch {\n\t\t\t// VideoDecoder HEVC not available or HEIC parsing failed.\n\t\t\t// Fall through to error.\n\t\t}\n\t}\n\n\tthrow new Error(\n\t\t'This browser cannot decode HEIC images. Please use Safari or convert to JPEG before uploading.'\n\t);\n}\n\n/**\n * Apply ISOBMFF irot rotation to a canvas.\n *\n * Returns the original canvas if no rotation is needed, or a new\n * OffscreenCanvas with the rotation applied.\n *\n * @param source Source canvas with the decoded image.\n * @param rotation Rotation angle in degrees counter-clockwise (0, 90, 180, 270).\n * @return Canvas with rotation applied.\n */\nfunction applyRotation(\n\tsource: OffscreenCanvas,\n\trotation: number\n): OffscreenCanvas {\n\tif ( rotation === 0 ) {\n\t\treturn source;\n\t}\n\n\tconst swap = rotation === 90 || rotation === 270;\n\tconst w = swap ? source.height : source.width;\n\tconst h = swap ? source.width : source.height;\n\n\tconst rotated = new OffscreenCanvas( w, h );\n\tconst ctx = rotated.getContext( '2d' );\n\n\tif ( ! ctx ) {\n\t\treturn source;\n\t}\n\n\tctx.translate( w / 2, h / 2 );\n\t// irot angle is CCW; canvas rotate() is CW, so negate.\n\tctx.rotate( ( -rotation * Math.PI ) / 180 );\n\tctx.drawImage( source, -source.width / 2, -source.height / 2 );\n\n\treturn rotated;\n}\n\n/**\n * Apply an EXIF orientation (1-8) to a canvas.\n *\n * Returns the original canvas for orientation 1, or a new OffscreenCanvas with\n * the rotation/flip applied. Orientations 5-8 swap width and height.\n *\n * @param source Source canvas with the decoded image.\n * @param orientation EXIF orientation value (1-8).\n * @return Canvas with the orientation applied.\n */\nfunction applyExifOrientation(\n\tsource: OffscreenCanvas,\n\torientation: number\n): OffscreenCanvas {\n\tif ( orientation <= 1 || orientation > 8 ) {\n\t\treturn source;\n\t}\n\n\tconst { width: sw, height: sh } = source;\n\tconst swap = orientation >= 5;\n\tconst out = new OffscreenCanvas( swap ? sh : sw, swap ? sw : sh );\n\tconst ctx = out.getContext( '2d' );\n\tif ( ! ctx ) {\n\t\treturn source;\n\t}\n\n\t// Affine transforms map EXIF orientation to the upright image. The e/f\n\t// translation terms use the source width/height. See the EXIF spec tag\n\t// 0x0112 and the standard orientation matrix.\n\tswitch ( orientation ) {\n\t\tcase 2: // Flip horizontal.\n\t\t\tctx.transform( -1, 0, 0, 1, sw, 0 );\n\t\t\tbreak;\n\t\tcase 3: // Rotate 180°.\n\t\t\tctx.transform( -1, 0, 0, -1, sw, sh );\n\t\t\tbreak;\n\t\tcase 4: // Flip vertical.\n\t\t\tctx.transform( 1, 0, 0, -1, 0, sh );\n\t\t\tbreak;\n\t\tcase 5: // Transpose.\n\t\t\tctx.transform( 0, 1, 1, 0, 0, 0 );\n\t\t\tbreak;\n\t\tcase 6: // Rotate 90° CW.\n\t\t\tctx.transform( 0, 1, -1, 0, sh, 0 );\n\t\t\tbreak;\n\t\tcase 7: // Transverse.\n\t\t\tctx.transform( 0, -1, -1, 0, sh, sw );\n\t\t\tbreak;\n\t\tcase 8: // Rotate 90° CCW.\n\t\t\tctx.transform( 0, -1, 1, 0, 0, sw );\n\t\t\tbreak;\n\t}\n\n\tctx.drawImage( source, 0, 0 );\n\treturn out;\n}\n\n/**\n * Decode a single HEVC key frame using the WebCodecs VideoDecoder API.\n *\n * @param codec HEVC codec string (e.g. 'hvc1.1.6.L93.B0').\n * @param description HEVCDecoderConfigurationRecord bytes.\n * @param width Coded width of the frame.\n * @param height Coded height of the frame.\n * @param data Raw HEVC bitstream (IDR frame).\n * @return Decoded VideoFrame. Caller must call frame.close().\n */\nfunction decodeHevcFrame(\n\tcodec: string,\n\tdescription: Uint8Array,\n\twidth: number,\n\theight: number,\n\tdata: Uint8Array\n): Promise< VideoFrame > {\n\treturn new Promise< VideoFrame >( ( resolve, reject ) => {\n\t\tconst decoder = new VideoDecoder( {\n\t\t\toutput: ( frame ) => {\n\t\t\t\tdecoder.close();\n\t\t\t\tresolve( frame );\n\t\t\t},\n\t\t\terror: ( e ) => {\n\t\t\t\tif ( decoder.state !== 'closed' ) {\n\t\t\t\t\tdecoder.close();\n\t\t\t\t}\n\t\t\t\treject( e );\n\t\t\t},\n\t\t} );\n\n\t\tdecoder.configure( {\n\t\t\tcodec,\n\t\t\tcodedWidth: width,\n\t\t\tcodedHeight: height,\n\t\t\tdescription,\n\t\t} );\n\n\t\tdecoder.decode(\n\t\t\tnew EncodedVideoChunk( {\n\t\t\t\ttype: 'key',\n\t\t\t\ttimestamp: 0,\n\t\t\t\tdata,\n\t\t\t} )\n\t\t);\n\n\t\tdecoder.flush().catch( ( e ) => {\n\t\t\tif ( decoder.state !== 'closed' ) {\n\t\t\t\tdecoder.close();\n\t\t\t}\n\t\t\treject( e );\n\t\t} );\n\t} );\n}\n"],
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,mBAAgC;AAChC,yBAA0B;AAoB1B,eAAsB,oBACrB,MACA,UAAU,MACQ;AAClB,QAAM,eAAW,8BAAiB,KAAK,IAAK;AAG5C,MAAI;AACH,UAAM,SAAS,MAAM,kBAAmB,IAAK;AAC7C,QAAI;AACH,YAAM,SAAS,IAAI,gBAAiB,OAAO,OAAO,OAAO,MAAO;AAChE,YAAM,MAAM,OAAO,WAAY,IAAK;AAEpC,UAAK,CAAE,KAAM;AACZ,cAAM,IAAI,MAAO,iCAAkC;AAAA,MACpD;AAEA,UAAI,UAAW,QAAQ,GAAG,CAAE;AAE5B,YAAM,WAAW,MAAM,OAAO,cAAe;AAAA,QAC5C,MAAM;AAAA,QACN;AAAA,MACD,CAAE;AAEF,aAAO,IAAI,KAAM,CAAE,QAAS,GAAG,GAAI,QAAS,QAAQ;AAAA,QACnD,MAAM;AAAA,MACP,CAAE;AAAA,IACH,UAAE;AACD,aAAO,MAAM;AAAA,IACd;AAAA,EACD,QAAQ;AAAA,EAGR;AAKA,MAAK,OAAO,iBAAiB,aAAc;AAC1C,UAAM,YAAY,MAAM,aAAa,gBAAiB,KAAK,IAAK;AAChE,QAAK,WAAY;AAChB,YAAM,UAAU,IAAI,aAAc;AAAA,QACjC,MAAM,KAAK;AAAA,QACX,MAAM,KAAK,OAAO;AAAA,MACnB,CAAE;AACF,UAAI;AACH,cAAM,EAAE,OAAO,WAAW,IAAI,MAAM,QAAQ,OAAO;AACnD,YAAI;AACH,gBAAM,SAAS,IAAI;AAAA,YAClB,WAAW;AAAA,YACX,WAAW;AAAA,UACZ;AACA,gBAAM,MAAM,OAAO,WAAY,IAAK;AAEpC,cAAK,CAAE,KAAM;AACZ,kBAAM,IAAI,MAAO,iCAAkC;AAAA,UACpD;AAEA,cAAI,UAAW,YAAY,GAAG,CAAE;AAEhC,gBAAM,WAAW,MAAM,OAAO,cAAe;AAAA,YAC5C,MAAM;AAAA,YACN;AAAA,UACD,CAAE;AAEF,iBAAO,IAAI,KAAM,CAAE,QAAS,GAAG,GAAI,QAAS,QAAQ;AAAA,YACnD,MAAM;AAAA,UACP,CAAE;AAAA,QACH,UAAE;AACD,qBAAW,MAAM;AAAA,QAClB;AAAA,MACD,UAAE;AACD,gBAAQ,MAAM;AAAA,MACf;AAAA,IACD;AAAA,EACD;AAOA,MAAK,OAAO,iBAAiB,aAAc;AAC1C,QAAI;AACH,YAAM,eAAW,8BAAW,MAAM,KAAK,YAAY,CAAE;AAErD,YAAM,UAAU,MAAM,aAAa,kBAAmB;AAAA,QACrD,OAAO,SAAS;AAAA,MACjB,CAAE;AAEF,UAAK,QAAQ,WAAY;AACxB,cAAM,SAAS,IAAI;AAAA,UAClB,SAAS;AAAA,UACT,SAAS;AAAA,QACV;AACA,cAAM,MAAM,OAAO,WAAY,IAAK;AAEpC,YAAK,CAAE,KAAM;AACZ,gBAAM,IAAI,MAAO,iCAAkC;AAAA,QACpD;AAGA,mBAAY,QAAQ,SAAS,OAAQ;AACpC,gBAAM,QAAQ,MAAM;AAAA,YACnB,SAAS;AAAA,YACT,SAAS;AAAA,YACT,SAAS;AAAA,YACT,SAAS;AAAA,YACT,KAAK;AAAA,UACN;AACA,cAAI;AACH,gBAAI,UAAW,OAAO,KAAK,GAAG,KAAK,CAAE;AAAA,UACtC,UAAE;AACD,kBAAM,MAAM;AAAA,UACb;AAAA,QACD;AAKA,cAAM,eACL,SAAS,aAAa,IACnB,cAAe,QAAQ,SAAS,QAAS,IACzC;AAAA,UACA;AAAA,UACA,SAAS;AAAA,QACT;AAEJ,cAAM,WAAW,MAAM,aAAa,cAAe;AAAA,UAClD,MAAM;AAAA,UACN;AAAA,QACD,CAAE;AAEF,eAAO,IAAI,KAAM,CAAE,QAAS,GAAG,GAAI,QAAS,QAAQ;AAAA,UACnD,MAAM;AAAA,QACP,CAAE;AAAA,MACH;AAAA,IACD,QAAQ;AAAA,IAGR;AAAA,EACD;AAEA,QAAM,IAAI;AAAA,IACT;AAAA,EACD;AACD;AAYA,SAAS,cACR,QACA,UACkB;AAClB,MAAK,aAAa,GAAI;AACrB,WAAO;AAAA,EACR;AAEA,QAAM,OAAO,aAAa,MAAM,aAAa;AAC7C,QAAM,IAAI,OAAO,OAAO,SAAS,OAAO;AACxC,QAAM,IAAI,OAAO,OAAO,QAAQ,OAAO;AAEvC,QAAM,UAAU,IAAI,gBAAiB,GAAG,CAAE;AAC1C,QAAM,MAAM,QAAQ,WAAY,IAAK;AAErC,MAAK,CAAE,KAAM;AACZ,WAAO;AAAA,EACR;AAEA,MAAI,UAAW,IAAI,GAAG,IAAI,CAAE;AAE5B,MAAI,OAAU,CAAC,WAAW,KAAK,KAAO,GAAI;AAC1C,MAAI,UAAW,QAAQ,CAAC,OAAO,QAAQ,GAAG,CAAC,OAAO,SAAS,CAAE;AAE7D,SAAO;AACR;AAYA,SAAS,qBACR,QACA,aACkB;AAClB,MAAK,eAAe,KAAK,cAAc,GAAI;AAC1C,WAAO;AAAA,EACR;AAEA,QAAM,EAAE,OAAO,IAAI,QAAQ,GAAG,IAAI;AAClC,QAAM,OAAO,eAAe;AAC5B,QAAM,MAAM,IAAI,gBAAiB,OAAO,KAAK,IAAI,OAAO,KAAK,EAAG;AAChE,QAAM,MAAM,IAAI,WAAY,IAAK;AACjC,MAAK,CAAE,KAAM;AACZ,WAAO;AAAA,EACR;AAKA,UAAS,aAAc;AAAA,IACtB,KAAK;AACJ,UAAI,UAAW,IAAI,GAAG,GAAG,GAAG,IAAI,CAAE;AAClC;AAAA,IACD,KAAK;AACJ,UAAI,UAAW,IAAI,GAAG,GAAG,IAAI,IAAI,EAAG;AACpC;AAAA,IACD,KAAK;AACJ,UAAI,UAAW,GAAG,GAAG,GAAG,IAAI,GAAG,EAAG;AAClC;AAAA,IACD,KAAK;AACJ,UAAI,UAAW,GAAG,GAAG,GAAG,GAAG,GAAG,CAAE;AAChC;AAAA,IACD,KAAK;AACJ,UAAI,UAAW,GAAG,GAAG,IAAI,GAAG,IAAI,CAAE;AAClC;AAAA,IACD,KAAK;AACJ,UAAI,UAAW,GAAG,IAAI,IAAI,GAAG,IAAI,EAAG;AACpC;AAAA,IACD,KAAK;AACJ,UAAI,UAAW,GAAG,IAAI,GAAG,GAAG,GAAG,EAAG;AAClC;AAAA,EACF;AAEA,MAAI,UAAW,QAAQ,GAAG,CAAE;AAC5B,SAAO;AACR;AAYA,SAAS,gBACR,OACA,aACA,OACA,QACA,MACwB;AACxB,SAAO,IAAI,QAAuB,CAAE,SAAS,WAAY;AACxD,UAAM,UAAU,IAAI,aAAc;AAAA,MACjC,QAAQ,CAAE,UAAW;AACpB,gBAAQ,MAAM;AACd,gBAAS,KAAM;AAAA,MAChB;AAAA,MACA,OAAO,CAAE,MAAO;AACf,YAAK,QAAQ,UAAU,UAAW;AACjC,kBAAQ,MAAM;AAAA,QACf;AACA,eAAQ,CAAE;AAAA,MACX;AAAA,IACD,CAAE;AAEF,YAAQ,UAAW;AAAA,MAClB;AAAA,MACA,YAAY;AAAA,MACZ,aAAa;AAAA,MACb;AAAA,IACD,CAAE;AAEF,YAAQ;AAAA,MACP,IAAI,kBAAmB;AAAA,QACtB,MAAM;AAAA,QACN,WAAW;AAAA,QACX;AAAA,MACD,CAAE;AAAA,IACH;AAEA,YAAQ,MAAM,EAAE,MAAO,CAAE,MAAO;AAC/B,UAAK,QAAQ,UAAU,UAAW;AACjC,gBAAQ,MAAM;AAAA,MACf;AACA,aAAQ,CAAE;AAAA,IACX,CAAE;AAAA,EACH,CAAE;AACH;",
"names": []
}